@yuneta/gobj-ui 7.26.33 → 7.26.35

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 (45) hide show
  1. package/README.md +223 -53
  2. package/dist/gobj-ui.cjs.js +1053 -144
  3. package/dist/gobj-ui.es.js +1053 -144
  4. package/package.json +2 -2
  5. package/scripts/validate-locales.mjs +672 -96
  6. package/src/c_yui_form.js +229 -11
  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 +156 -44
  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/form_table_validator.wiring.test.js +192 -0
  23. package/src/form_template_field.wiring.test.js +21 -0
  24. package/src/i18n_key_functions.js +14 -6
  25. package/src/i18n_key_functions.test.js +124 -2
  26. package/src/lib_maplibre.js +26 -2
  27. package/src/lib_maplibre_relocalize.test.js +78 -0
  28. package/src/named_controls.wiring.test.js +468 -0
  29. package/src/nav_cards_helpers.js +9 -1
  30. package/src/nav_item_badge.wiring.test.js +182 -1
  31. package/src/shell_route_map.js +25 -8
  32. package/src/table_editor_name.wiring.test.js +201 -0
  33. package/src/treedb_column_title.wiring.test.js +151 -0
  34. package/src/treedb_json_drill.wiring.test.js +41 -1
  35. package/src/window_layer.wiring.test.js +308 -43
  36. package/src/window_show.wiring.test.js +6 -2
  37. package/src/window_stacking.wiring.test.js +334 -0
  38. package/src/yui_badge.js +58 -13
  39. package/src/yui_badge.test.js +40 -3
  40. package/src/yui_dev.js +2 -1
  41. package/src/yui_file_field.js +2 -0
  42. package/src/yui_frontend_view.js +2 -1
  43. package/src/yui_gclass_view.js +9 -6
  44. package/src/yui_tabulator_i18n.js +42 -0
  45. package/src/yui_tabulator_i18n.test.js +48 -1
@@ -540,14 +540,26 @@ function text_list_item_formatter(label) {
540
540
  * ALWAYS RENDERED, hidden when empty, so the runtime setters have a
541
541
  * node to write into and never rebuild the control that holds it.
542
542
  *
543
- * role="status" and NOT aria-hidden, deliberately. The control
544
- * carries an explicit aria-label, and an explicit label REPLACES
545
- * the element's content for a screen reader -- so a badge inside
546
- * it would be silent. As its own live region it is both read and
547
- * announced when the count changes, which is the whole point of a
548
- * badge that means "something needs you". For the same reason a
549
- * write that changes nothing touches no DOM: rewriting the same
550
- * number would have it announced again on every tick.
543
+ * PART OF THE CONTROL'S NAME (name_control_with_badge()). The
544
+ * control carries an explicit aria-label, and an explicit label
545
+ * REPLACES the element's content for a screen reader -- so a badge
546
+ * inside it is silent, and inside a <button> (the toolbar, an
547
+ * accordion head) its children are not even in the tree. The
548
+ * control is labelled by ITSELF (its aria-label, which
549
+ * refresh_language() re-translates) and by the badge: the name is
550
+ * "Alarms 3" while there is a count, "Alarms" while there is none,
551
+ * with nothing composed in javascript that a language switch would
552
+ * leave behind.
553
+ *
554
+ * NOT a live region any more. Up to 7.26.34 the badge was its own
555
+ * role="status", and that said "3" with no word of what has three;
556
+ * hidden while empty, it was not in the tree when the first count
557
+ * arrived, so 0 -> 1 was most likely never said; and inside a
558
+ * <button> it was not there at all. The change is said by the
559
+ * shell, in one live region of its own that is always there, with
560
+ * the name of the item: "Alarms, 3" (yui_shell_set_nav_item_badge,
561
+ * yui_shell_set_toolbar_item_badge). A write that changes nothing
562
+ * still touches no DOM.
551
563
  *
552
564
  * A TONE, optional: one of BADGE_TONES, drawn as Bulma's
553
565
  * `is-<tone>` on the badge. It says HOW BAD the count is -- the
@@ -573,6 +585,7 @@ var BADGE_TONES = Object.freeze([
573
585
  "primary",
574
586
  "link"
575
587
  ]);
588
+ var __badge_seq__ = 0;
576
589
  /************************************************************
577
590
  * The text a badge shows for `value`, "" when it shows none.
578
591
  ************************************************************/
@@ -606,10 +619,7 @@ function badge_tone(tone, who) {
606
619
  function badge_node(value, tone, classes, who) {
607
620
  let text = badge_text(value);
608
621
  let known = badge_tone(tone, who);
609
- let attrs = {
610
- class: known ? `${classes} is-${known}` : classes,
611
- role: "status"
612
- };
622
+ let attrs = { class: known ? `${classes} is-${known}` : classes };
613
623
  if (!text) attrs.hidden = "hidden";
614
624
  return [
615
625
  "span",
@@ -618,6 +628,31 @@ function badge_node(value, tone, classes, who) {
618
628
  ];
619
629
  }
620
630
  /************************************************************
631
+ * Make the count part of the NAME of the control that carries
632
+ * the badge: its `aria-labelledby` points at the control itself
633
+ * and at the badge.
634
+ *
635
+ * The control keeps its `aria-label` and its
636
+ * data-i18n-aria-label: a self-reference reads the aria-label,
637
+ * so the name is the translated label followed by the badge's
638
+ * text -- "Alarms 3" -- or the label alone while the badge is
639
+ * empty. A painted count is in the name at once, and a language
640
+ * switch re-translates the label as it always did.
641
+ *
642
+ * Both are createElement2() descriptions, before anything is
643
+ * built: `control_attrs` (the attrs of the <a> or <button>, which
644
+ * must carry its aria-label) gets an `id` when it has none, and
645
+ * the `aria-labelledby`; `badge` (what badge_node() answered)
646
+ * gets its `id`.
647
+ ************************************************************/
648
+ function name_control_with_badge(control_attrs, badge) {
649
+ let seq = ++__badge_seq__;
650
+ if (!control_attrs.id) control_attrs.id = `yui-badged-${seq}`;
651
+ let badge_attrs = badge[1];
652
+ badge_attrs.id = `yui-badge-${seq}`;
653
+ control_attrs["aria-labelledby"] = `${control_attrs.id} ${badge_attrs.id}`;
654
+ }
655
+ /************************************************************
621
656
  * Write a badge that is already in the DOM, as badge_node()
622
657
  * draws it. `tone` is one badge_tone() answered: the setter
623
658
  * checks it once for all the badges it paints.
@@ -1014,6 +1049,8 @@ function secondary_nav_renders(item, layout) {
1014
1049
  *
1015
1050
  * Parses a JSON config to build:
1016
1051
  * - Layers (z-stacked): base, overlay, popup, modal, notification, loading
1052
+ * (the overlay holds the WINDOW layer, where the windows live,
1053
+ * under the drawer: yui_shell_window_layer())
1017
1054
  * - Zones (inside base layer): top, top-sub, left, center, right,
1018
1055
  * bottom-sub, bottom
1019
1056
  * - Menus (primary + submenus) rendered via C_YUI_NAV — one nav
@@ -1041,6 +1078,8 @@ function secondary_nav_renders(item, layout) {
1041
1078
  ***************************************************************/
1042
1079
  var GCLASS_NAME$22 = "C_YUI_SHELL";
1043
1080
  var __last_shell__ = null;
1081
+ var __shell_watchers__ = [];
1082
+ var __window_z__ = 10;
1044
1083
  var ZONE_IDS = [
1045
1084
  "top",
1046
1085
  "top-sub",
@@ -1075,10 +1114,12 @@ var attrs_table$22 = [
1075
1114
  var PRIVATE_DATA$22 = {
1076
1115
  zones: {},
1077
1116
  layers: {},
1117
+ window_layer: null,
1078
1118
  stages: {},
1079
1119
  navs: [],
1080
1120
  item_index: {},
1081
1121
  nav_badges: {},
1122
+ badge_status: null,
1082
1123
  dynamic_items: {},
1083
1124
  section_pos: {},
1084
1125
  route_mru: [],
@@ -1119,6 +1160,7 @@ function mt_create$22(gobj) {
1119
1160
  if (subscriber) gobj_subscribe_event(gobj, null, {}, subscriber);
1120
1161
  __last_shell__ = gobj;
1121
1162
  build_ui$19(gobj);
1163
+ tell_shell_watchers(gobj);
1122
1164
  }
1123
1165
  /***************************************************************
1124
1166
  * Framework Method: Start
@@ -1302,10 +1344,11 @@ function mt_stop$20(gobj) {
1302
1344
  ***************************************************************/
1303
1345
  function mt_destroy$22(gobj) {
1304
1346
  close_toolbar_dropdown(gobj);
1347
+ if (__last_shell__ === gobj) __last_shell__ = null;
1305
1348
  let $container = gobj_read_attr(gobj, "$container");
1306
1349
  if ($container && $container.parentNode) $container.parentNode.removeChild($container);
1350
+ tell_shell_watchers(gobj);
1307
1351
  gobj_write_attr(gobj, "$container", null);
1308
- if (__last_shell__ === gobj) __last_shell__ = null;
1309
1352
  }
1310
1353
  /***************************
1311
1354
  * Local Methods
@@ -1428,6 +1471,17 @@ function build_ui$19(gobj) {
1428
1471
  $container.appendChild($layer);
1429
1472
  priv.layers[id] = $layer;
1430
1473
  }
1474
+ let $windows = createElement2(["div", { class: "SHELL_WINDOW_LAYER yui-window-layer" }]);
1475
+ priv.layers.overlay.appendChild($windows);
1476
+ priv.window_layer = $windows;
1477
+ let $status = createElement2(["div", {
1478
+ class: "SHELL_BADGE_STATUS is-sr-only",
1479
+ role: "status",
1480
+ "aria-live": "polite",
1481
+ "aria-atomic": "true"
1482
+ }]);
1483
+ $container.appendChild($status);
1484
+ priv.badge_status = $status;
1431
1485
  let $base = priv.layers.base;
1432
1486
  $base.classList.add("yui-base-grid");
1433
1487
  let zones_cfg = shell_cfg.zones || {};
@@ -2021,14 +2075,18 @@ function toolbar_link_attrs(it) {
2021
2075
  ************************************************************/
2022
2076
  function build_toolbar_action_item(gobj, it) {
2023
2077
  let children = [];
2024
- if (!empty_string(it.icon)) children.push([
2025
- "span",
2026
- { class: "icon yui-toolbar-icon" },
2027
- [["i", {
2028
- class: it.icon,
2029
- "aria-hidden": "true"
2030
- }], badge_node(it.badge, it.badge_tone, "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge", `C_YUI_SHELL: toolbar item '${it.id || "?"}'`)]
2031
- ]);
2078
+ let badge = null;
2079
+ if (!empty_string(it.icon)) {
2080
+ badge = badge_node(it.badge, it.badge_tone, "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge", `C_YUI_SHELL: toolbar item '${it.id || "?"}'`);
2081
+ children.push([
2082
+ "span",
2083
+ { class: "icon yui-toolbar-icon" },
2084
+ [["i", {
2085
+ class: it.icon,
2086
+ "aria-hidden": "true"
2087
+ }], badge]
2088
+ ]);
2089
+ }
2032
2090
  if (!empty_string(it.name)) children.push([
2033
2091
  "span",
2034
2092
  {
@@ -2055,6 +2113,7 @@ function build_toolbar_action_item(gobj, it) {
2055
2113
  btn_attrs.title = tip;
2056
2114
  btn_attrs["data-i18n-title"] = tip;
2057
2115
  }
2116
+ if (badge && !empty_string(aria_key)) name_control_with_badge(btn_attrs, badge);
2058
2117
  let link = toolbar_link_attrs(it);
2059
2118
  if (link) {
2060
2119
  delete btn_attrs.type;
@@ -2839,23 +2898,32 @@ function update_secondary_nav_visibility(gobj, entry) {
2839
2898
  }
2840
2899
  }
2841
2900
  /************************************************************
2842
- * Does a menu of this shell declare an item `item_id`: an item
2843
- * of a menu of app_config.json, of one of their submenus, or
2844
- * one given to yui_shell_set_submenu() since.
2901
+ * The item `item_id` a menu of this shell declares, null when
2902
+ * none does: an item of a menu of app_config.json, of one of
2903
+ * their submenus, or one given to yui_shell_set_submenu() since
2904
+ * -- which comes first, because it replaced what the submenu
2905
+ * declared and is what the navs draw.
2845
2906
  ************************************************************/
2846
- function nav_item_declared(gobj, item_id) {
2907
+ function nav_item_of(gobj, item_id) {
2847
2908
  let priv = gobj.priv;
2848
2909
  let config = gobj_read_attr(gobj, "config") || {};
2849
2910
  let menus = is_object(config.menu) ? config.menu : {};
2850
- let holds = (items) => is_array(items) && items.some((it) => it && it.id === item_id);
2911
+ let find = (items) => is_array(items) && items.find((it) => it && it.id === item_id) || null;
2912
+ for (let parent_item_id in priv.dynamic_items) {
2913
+ let it = find(priv.dynamic_items[parent_item_id]);
2914
+ if (it) return it;
2915
+ }
2851
2916
  for (let menu_id in menus) {
2852
2917
  let items = menus[menu_id] && menus[menu_id].items;
2853
2918
  if (!is_array(items)) continue;
2854
- if (holds(items)) return true;
2855
- for (let it of items) if (it && it.submenu && holds(it.submenu.items)) return true;
2919
+ let it = find(items);
2920
+ if (it) return it;
2921
+ for (let parent of items) {
2922
+ let sub = parent && parent.submenu && find(parent.submenu.items);
2923
+ if (sub) return sub;
2924
+ }
2856
2925
  }
2857
- for (let parent_item_id in priv.dynamic_items) if (holds(priv.dynamic_items[parent_item_id])) return true;
2858
- return false;
2926
+ return null;
2859
2927
  }
2860
2928
  /************************************************************
2861
2929
  * Every C_YUI_NAV of this shell: the navs of its menus, and the
@@ -2873,6 +2941,61 @@ function shell_navs(gobj) {
2873
2941
  }
2874
2942
  return navs;
2875
2943
  }
2944
+ /************************************************************
2945
+ * Say a badge that changed, with the name of what it counts:
2946
+ * "Alarms, 3", in the shell's live region (build_ui()).
2947
+ *
2948
+ * The badge itself is no live region (yui_badge.js): its number
2949
+ * alone said nothing, hidden while empty it missed the first
2950
+ * count, and inside a <button> it was not there at all. The
2951
+ * setters call this only when the text a reader would see
2952
+ * changes and is not empty: a count going away is not news, and
2953
+ * the same count again is not either.
2954
+ *
2955
+ * `name_key` is the key of the control's name; the phrase is a
2956
+ * key of its own, so a language orders and punctuates it its
2957
+ * way. Said once: a live region needs no re-translation.
2958
+ ************************************************************/
2959
+ function announce_badge(gobj, name_key, text) {
2960
+ let priv = gobj.priv;
2961
+ let $status = priv.badge_status;
2962
+ if (!$status) {
2963
+ log_error(`C_YUI_SHELL: no live region to say badge '${name_key}': ${text}`);
2964
+ return;
2965
+ }
2966
+ let message;
2967
+ if (typeof priv.translator === "function") {
2968
+ const tr = priv.translator;
2969
+ message = tr("{{name}}, {{n}}", {
2970
+ name: tr(name_key),
2971
+ n: text
2972
+ });
2973
+ } else message = `${name_key}, ${text}`;
2974
+ $status.textContent = message;
2975
+ }
2976
+ /************************************************************
2977
+ * A shell was built (mt_create) or is being destroyed
2978
+ * (mt_destroy): tell every gobj that keeps DOM in a shell's
2979
+ * layers without being its child (yui_shell_watch_changes()).
2980
+ * Each one decides in its own action what it means for it --
2981
+ * the trace shows EV_SHELL_CHANGED arriving, from a `!!` shell
2982
+ * when it is the one going.
2983
+ *
2984
+ * An event, not a call into the window's module: the shell does
2985
+ * not import its watchers, and the move is work, so it crosses
2986
+ * their FSM. A watcher that is being destroyed is one whose
2987
+ * mt_destroy did not unwatch: it is dropped, and said.
2988
+ ************************************************************/
2989
+ function tell_shell_watchers(gobj) {
2990
+ for (let watcher of __shell_watchers__.slice()) {
2991
+ if (gobj_is_destroying(watcher)) {
2992
+ log_error(`C_YUI_SHELL: a destroyed gobj still watches the shell (${gobj_short_name(watcher)}): it must call yui_shell_unwatch_changes() in its mt_destroy`);
2993
+ yui_shell_unwatch_changes(watcher);
2994
+ continue;
2995
+ }
2996
+ gobj_send_event(watcher, "EV_SHELL_CHANGED", {}, gobj);
2997
+ }
2998
+ }
2876
2999
  /***************************
2877
3000
  * Actions
2878
3001
  ***************************/
@@ -3142,6 +3265,81 @@ function yui_shell_of(gobj) {
3142
3265
  return __last_shell__;
3143
3266
  }
3144
3267
  /************************************************************
3268
+ * The DOM where a C_YUI_WINDOW lives when its host names no
3269
+ * `$parent`: the WINDOW layer, a stacking context of its own
3270
+ * inside the overlay layer -- above the content, below the
3271
+ * drawer, the toolbar's dropdowns, the dialogs and the toasts,
3272
+ * whatever z-index a window takes there. Null with no shell (the
3273
+ * window then lives in document.body).
3274
+ ************************************************************/
3275
+ function yui_shell_window_layer(shell_gobj) {
3276
+ if (!shell_gobj || !shell_gobj.priv) return null;
3277
+ let priv = shell_gobj.priv;
3278
+ return priv.window_layer || priv.layers && priv.layers.overlay || null;
3279
+ }
3280
+ /************************************************************
3281
+ * Put a window on top of the others: the next number of ONE
3282
+ * counter, the same for a window of the dock and for a floating
3283
+ * one.
3284
+ *
3285
+ * Up to 7.26.34 there were two orders: the dock raised its windows
3286
+ * to a counter of its own (11, 12, ...), and a window without a
3287
+ * dock sat at 3 and was raised by being moved to the end of its
3288
+ * parent -- so a window opened LATER without a dock (a treedb's
3289
+ * raw json) opened UNDER any docked one, and no click could bring
3290
+ * it up. They share the window layer, and this one number orders
3291
+ * them all. It grows for ever, and it can: nothing else lives in
3292
+ * the window layer's stacking context.
3293
+ *
3294
+ * A number, not a move in the DOM: moving an element takes its
3295
+ * focus, its scroll positions and the pointer press that started
3296
+ * on it.
3297
+ ************************************************************/
3298
+ function yui_shell_raise_window($el) {
3299
+ if (!$el || !$el.style) {
3300
+ log_error("C_YUI_SHELL: yui_shell_raise_window() needs an element");
3301
+ return;
3302
+ }
3303
+ __window_z__ += 1;
3304
+ $el.style.zIndex = String(__window_z__);
3305
+ }
3306
+ /************************************************************
3307
+ * Ask to be told (EV_SHELL_CHANGED, an INPUT event the gclass of
3308
+ * `gobj` must take in its FSM) when a shell is built or
3309
+ * destroyed.
3310
+ *
3311
+ * For a gobj that keeps DOM in a shell's layers without being
3312
+ * its child -- a window parented to the app, the dock. A shell's
3313
+ * children are destroyed with it; such a gobj is not, and its
3314
+ * place goes with the shell: a logout destroys the shell, the
3315
+ * login builds another. Told, it takes its place again in the
3316
+ * action.
3317
+ *
3318
+ * Idempotent. Pair it with yui_shell_unwatch_changes() in
3319
+ * mt_destroy, or the next shell sends the event to a destroyed
3320
+ * gobj.
3321
+ *
3322
+ * function mt_create(gobj) {
3323
+ * ...
3324
+ * yui_shell_watch_changes(gobj);
3325
+ * }
3326
+ * function mt_destroy(gobj) {
3327
+ * yui_shell_unwatch_changes(gobj);
3328
+ * ...
3329
+ * }
3330
+ ************************************************************/
3331
+ function yui_shell_watch_changes(gobj) {
3332
+ if (!is_gobj(gobj)) {
3333
+ log_error("C_YUI_SHELL: yui_shell_watch_changes() needs a gobj");
3334
+ return;
3335
+ }
3336
+ if (__shell_watchers__.indexOf(gobj) < 0) __shell_watchers__.push(gobj);
3337
+ }
3338
+ function yui_shell_unwatch_changes(gobj) {
3339
+ let idx = __shell_watchers__.indexOf(gobj);
3340
+ if (idx >= 0) __shell_watchers__.splice(idx, 1);
3341
+ }
3342
+ /************************************************************
3145
3343
  * Programmatic navigation.
3146
3344
  *
3147
3345
  * Default — PUSH: the user MOVED somewhere new, so change the URL via
@@ -3397,7 +3595,13 @@ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value, tone) {
3397
3595
  if (!$container) return;
3398
3596
  let $badge = $container.querySelector(`[data-toolbar-item-id="${item_id}"] .yui-toolbar-badge`);
3399
3597
  if (!$badge) return;
3598
+ let before = $badge.hasAttribute("hidden") ? "" : $badge.textContent;
3400
3599
  paint_badge($badge, value, tone);
3600
+ let now = badge_text(value);
3601
+ if (now && now !== before) {
3602
+ let $item = $badge.closest("[data-toolbar-item-id]");
3603
+ announce_badge(shell_gobj, $item && $item.getAttribute("data-i18n-aria-label") || item_id, now);
3604
+ }
3401
3605
  }
3402
3606
  /************************************************************
3403
3607
  * Set (or clear) the badge of a MENU item — the twin of
@@ -3431,8 +3635,11 @@ function yui_shell_set_nav_item_badge(shell_gobj, item_id, value, tone) {
3431
3635
  tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_nav_item_badge('${item_id}')`);
3432
3636
  if (!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj) || empty_string(item_id)) return;
3433
3637
  let priv = shell_gobj.priv;
3434
- if (!priv || !nav_item_declared(shell_gobj, item_id)) return;
3638
+ let item = priv ? nav_item_of(shell_gobj, item_id) : null;
3639
+ if (!item) return;
3435
3640
  let badge = value === void 0 ? null : value;
3641
+ let was = priv.nav_badges[item_id];
3642
+ let before = badge_text(was ? was.badge : item.badge);
3436
3643
  priv.nav_badges[item_id] = {
3437
3644
  badge,
3438
3645
  badge_tone: tone
@@ -3442,6 +3649,8 @@ function yui_shell_set_nav_item_badge(shell_gobj, item_id, value, tone) {
3442
3649
  badge,
3443
3650
  badge_tone: tone
3444
3651
  }, shell_gobj);
3652
+ let now = badge_text(badge);
3653
+ if (now && now !== before) announce_badge(shell_gobj, item.name || item_id, now);
3445
3654
  }
3446
3655
  /************************************************************
3447
3656
  * Programmatic close of any open toolbar dropdown. Useful for
@@ -4045,7 +4254,7 @@ var MOBILE_MAX = 768;
4045
4254
  ***************************************************************/
4046
4255
  var attrs_table$21 = [
4047
4256
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
4048
- SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else to the overlay layer of the shell (document.body when there is none)"),
4257
+ SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else to the window layer of the shell (document.body while there is none), following the shell when it is destroyed or another is built"),
4049
4258
  SDATA(data_type_t.DTP_INTEGER, "x", 0, "300", "X position"),
4050
4259
  SDATA(data_type_t.DTP_INTEGER, "y", 0, "100", "Y position"),
4051
4260
  SDATA(data_type_t.DTP_INTEGER, "width", 0, "700", "Width of the window"),
@@ -4081,7 +4290,11 @@ var attrs_table$21 = [
4081
4290
  SDATA(data_type_t.DTP_POINTER, "win_resize_handler", 0, null, "Internal: native window 'resize' listener"),
4082
4291
  SDATA_END()
4083
4292
  ];
4084
- var PRIVATE_DATA$21 = { prevSize: 0 };
4293
+ var PRIVATE_DATA$21 = {
4294
+ prevSize: 0,
4295
+ placed_by_shell: false,
4296
+ back_shell: null
4297
+ };
4085
4298
  var __gclass__$21 = null;
4086
4299
  /******************************
4087
4300
  * Framework Methods
@@ -4096,6 +4309,7 @@ function mt_create$21(gobj) {
4096
4309
  gobj_write_attr(gobj, "window_id", window_id);
4097
4310
  let manager = resolve_manager(gobj);
4098
4311
  build_ui$18(gobj);
4312
+ yui_shell_watch_changes(gobj);
4099
4313
  if (manager) {
4100
4314
  let $c = gobj_read_attr(gobj, "$container");
4101
4315
  if ($c) $c.addEventListener("pointerdown", function() {
@@ -4111,7 +4325,16 @@ function mt_create$21(gobj) {
4111
4325
  title_prefix: gobj_read_str_attr(gobj, "title_prefix") || "",
4112
4326
  icon: gobj_read_attr(gobj, "icon") || ""
4113
4327
  }, gobj);
4114
- } else if (gobj_read_bool_attr(gobj, "back_dismissable")) register_back_overlay(gobj);
4328
+ } else {
4329
+ let $c = gobj_read_attr(gobj, "$container");
4330
+ if ($c) {
4331
+ yui_shell_raise_window($c);
4332
+ $c.addEventListener("pointerdown", function() {
4333
+ if (!gobj_is_destroying(gobj)) gobj_send_event(gobj, "EV_SHOW", {}, gobj);
4334
+ }, true);
4335
+ }
4336
+ if (gobj_read_bool_attr(gobj, "back_dismissable")) register_back_overlay(gobj);
4337
+ }
4115
4338
  let on_win_resize = function() {
4116
4339
  handleResize(gobj);
4117
4340
  };
@@ -4122,13 +4345,15 @@ function mt_create$21(gobj) {
4122
4345
  * Framework Method: Destroy
4123
4346
  ***************************************************************/
4124
4347
  function mt_destroy$21(gobj) {
4348
+ let priv = gobj.priv;
4349
+ yui_shell_unwatch_changes(gobj);
4125
4350
  let manager = gobj_read_pointer_attr(gobj, "manager");
4126
4351
  if (manager) gobj_send_event(manager, "EV_UNREGISTER_WINDOW", { window: gobj_full_name(gobj) }, gobj);
4127
4352
  let back_overlay = gobj_read_attr(gobj, "back_overlay");
4128
4353
  if (back_overlay) {
4129
- let shell = yui_shell_of(gobj);
4130
- if (shell) yui_shell_overlay_dismissed(shell, back_overlay);
4354
+ if (priv.back_shell) yui_shell_overlay_dismissed(priv.back_shell, back_overlay);
4131
4355
  gobj_write_attr(gobj, "back_overlay", null);
4356
+ priv.back_shell = null;
4132
4357
  }
4133
4358
  let on_win_resize = gobj_read_attr(gobj, "win_resize_handler");
4134
4359
  if (on_win_resize) {
@@ -4540,7 +4765,8 @@ function build_ui$18(gobj) {
4540
4765
  }
4541
4766
  refresh_language($container, t);
4542
4767
  let $parent = gobj_read_attr(gobj, "$parent");
4543
- if (!$parent) $parent = default_parent(gobj);
4768
+ gobj.priv.placed_by_shell = !$parent;
4769
+ if (!$parent) $parent = window_place(gobj);
4544
4770
  $parent.appendChild($container);
4545
4771
  if (gobj_read_bool_attr(gobj, "openMaximized")) max$2(gobj);
4546
4772
  else if (!mobile) {
@@ -4582,11 +4808,13 @@ function register_back_overlay(gobj) {
4582
4808
  };
4583
4809
  let overlay = yui_shell_register_overlay(shell, function() {
4584
4810
  gobj_write_attr(gobj, "back_overlay", null);
4811
+ gobj.priv.back_shell = null;
4585
4812
  const closing = close_window(gobj);
4586
4813
  if (closing.state === "vetoed") rearm();
4587
4814
  else if (closing.state === "asking") closing.answer.then(rearm);
4588
4815
  }, { keep_on_navigate: gobj_read_bool_attr(gobj, "keep_on_navigate") });
4589
4816
  gobj_write_attr(gobj, "back_overlay", overlay);
4817
+ gobj.priv.back_shell = overlay ? shell : null;
4590
4818
  }
4591
4819
  /************************************************************
4592
4820
  * Ask to close the window. Answers what happened:
@@ -4628,7 +4856,8 @@ function close_window(gobj) {
4628
4856
  }
4629
4857
  /************************************************************
4630
4858
  * Where a window lives when its host names no `$parent`: the
4631
- * OVERLAY layer of the shell -- above the content, below the
4859
+ * WINDOW layer of its shell (yui_shell_window_layer()), inside
4860
+ * the overlay layer -- above the content, below the drawer, the
4632
4861
  * toolbar's dropdowns, the dialogs and the toasts.
4633
4862
  *
4634
4863
  * It used to be document.body, and that put every window above
@@ -4641,14 +4870,17 @@ function close_window(gobj) {
4641
4870
  * toolbar, any dialog opened with a window on screen, the same.
4642
4871
  *
4643
4872
  * Inside the layer the dock's z-index still orders the windows
4644
- * among themselves. No shell (a page with windows and no shell):
4645
- * the body, as before.
4873
+ * among themselves, and no number it gives them reaches the
4874
+ * drawer (7.26.31 put them in the overlay layer itself, beside
4875
+ * it). No shell (a page with windows and no shell, or the time
4876
+ * between a logout and the next login): the body.
4877
+ *
4878
+ * The place is the shell's, and a window parented to the app
4879
+ * outlives it: ac_shell_changed() moves it when a shell goes or
4880
+ * comes.
4646
4881
  ************************************************************/
4647
- function default_parent(gobj) {
4648
- let shell = yui_shell_of(gobj);
4649
- let layers = shell && shell.priv && shell.priv.layers;
4650
- if (layers && layers.overlay) return layers.overlay;
4651
- return document.body;
4882
+ function window_place(gobj) {
4883
+ return yui_shell_window_layer(yui_shell_of(gobj)) || document.body;
4652
4884
  }
4653
4885
  /************************************************************
4654
4886
  *
@@ -4920,8 +5152,7 @@ function ac_show$15(gobj, event, kw, src) {
4920
5152
  return -1;
4921
5153
  }
4922
5154
  $container.style.removeProperty("display");
4923
- let $parent = $container.parentNode;
4924
- if ($parent && $parent.lastChild !== $container) $parent.appendChild($container);
5155
+ yui_shell_raise_window($container);
4925
5156
  return 0;
4926
5157
  }
4927
5158
  /************************************************************
@@ -4938,6 +5169,49 @@ function ac_close(gobj, event, kw, src) {
4938
5169
  close_window(gobj);
4939
5170
  return 0;
4940
5171
  }
5172
+ /************************************************************
5173
+ * EV_SHELL_CHANGED -- a shell was built, or is being destroyed
5174
+ * (`src`, then destroying): the place this window lives in may
5175
+ * have gone with it, or be in the new one
5176
+ * (yui_shell_watch_changes()).
5177
+ *
5178
+ * - A window whose host named no `$parent` takes the window place
5179
+ * again: the window layer of the shell that governs it NOW,
5180
+ * document.body while there is none. So the developer window
5181
+ * outlives a logout in the body, and the login's shell takes it
5182
+ * in -- above its content, under its dialogs. From 7.26.31 to
5183
+ * 7.26.34 it stayed in the destroyed shell's layer: alive,
5184
+ * detached, invisible.
5185
+ * - A window its host placed stays where it was put, unless that
5186
+ * element was inside the shell being destroyed: then no place
5187
+ * is left, and it takes the window place from now on.
5188
+ * - Its Back entry, when the shell being destroyed held it, went
5189
+ * with that shell's history and is forgotten. The next shell
5190
+ * does not get one: a history entry pushed while a shell is
5191
+ * still being built would sit under its first route. Back no
5192
+ * longer closes such a window; its ✕ does, as before 7.26.31.
5193
+ ************************************************************/
5194
+ function ac_shell_changed$1(gobj, event, kw, src) {
5195
+ let priv = gobj.priv;
5196
+ let going = gobj_is_destroying(src);
5197
+ if (going && priv.back_shell === src) {
5198
+ gobj_write_attr(gobj, "back_overlay", null);
5199
+ priv.back_shell = null;
5200
+ }
5201
+ let $container = gobj_read_attr(gobj, "$container");
5202
+ if (!$container) {
5203
+ log_error(`${gobj_short_name(gobj)}: EV_SHELL_CHANGED without a container`);
5204
+ return -1;
5205
+ }
5206
+ if (!priv.placed_by_shell) {
5207
+ let $shell = going ? gobj_read_attr(src, "$container") : null;
5208
+ if (!$shell || !$shell.contains($container)) return 0;
5209
+ priv.placed_by_shell = true;
5210
+ }
5211
+ let $place = window_place(gobj);
5212
+ if ($container.parentNode !== $place) $place.appendChild($container);
5213
+ return 0;
5214
+ }
4941
5215
  /***************************
4942
5216
  * FSM
4943
5217
  ***************************/
@@ -4973,6 +5247,11 @@ function create_gclass$21(gclass_name) {
4973
5247
  "EV_CLOSE_WINDOW",
4974
5248
  ac_close,
4975
5249
  null
5250
+ ],
5251
+ [
5252
+ "EV_SHELL_CHANGED",
5253
+ ac_shell_changed$1,
5254
+ null
4976
5255
  ]
4977
5256
  ]]];
4978
5257
  const event_types = [
@@ -4982,7 +5261,8 @@ function create_gclass$21(gclass_name) {
4982
5261
  ["EV_REFRESH", 0],
4983
5262
  ["EV_SHOW", 0],
4984
5263
  ["EV_HIDE", 0],
4985
- ["EV_CLOSE_WINDOW", 0]
5264
+ ["EV_CLOSE_WINDOW", 0],
5265
+ ["EV_SHELL_CHANGED", 0]
4986
5266
  ];
4987
5267
  __gclass__$21 = gclass_create(gclass_name, event_types, states, gmt$21, 0, attrs_table$21, PRIVATE_DATA$21, 0, 0, 0, 0);
4988
5268
  if (!__gclass__$21) return -1;
@@ -5033,6 +5313,7 @@ function register_c_yui_window() {
5033
5313
  var GCLASS_NAME$20 = "C_YUI_WINDOW_MANAGER";
5034
5314
  var DOCK_Z = 1e6;
5035
5315
  var WC_X = "<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M4.5 4.5 L11.5 11.5 M11.5 4.5 L4.5 11.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>";
5316
+ var __chip_seq__ = 0;
5036
5317
  /***************************************************************
5037
5318
  * Data
5038
5319
  ***************************************************************/
@@ -5042,13 +5323,12 @@ var attrs_table$20 = [
5042
5323
  SDATA(data_type_t.DTP_STRING, "dock_corner", 0, "bottom-left", "Floating placement: top-left | top-right | bottom-left | bottom-right"),
5043
5324
  SDATA(data_type_t.DTP_STRING, "inline_selector", 0, "", "CSS selector of the container the dock mounts into when inline (e.g. a shell zone)"),
5044
5325
  SDATA(data_type_t.DTP_STRING, "responsive_query", 0, "(max-width: 768px)", "Media query that selects inline placement when dock_mode is 'responsive'"),
5045
- SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "Where the floating dock mounts (default document.body)"),
5326
+ SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "Where the floating dock mounts (default: the popup layer of the shell, document.body while there is none)"),
5046
5327
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: the dock element"),
5047
5328
  SDATA_END()
5048
5329
  ];
5049
5330
  var PRIVATE_DATA$20 = {
5050
5331
  windows: null,
5051
- z: 0,
5052
5332
  mql: null,
5053
5333
  mql_handler: null
5054
5334
  };
@@ -5062,7 +5342,6 @@ var __gclass__$20 = null;
5062
5342
  function mt_create$20(gobj) {
5063
5343
  let priv = gobj.priv;
5064
5344
  priv.windows = [];
5065
- priv.z = 10;
5066
5345
  if ((gobj_read_attr(gobj, "dock_mode") || "floating") === "responsive") {
5067
5346
  let q = gobj_read_attr(gobj, "responsive_query") || "(max-width: 768px)";
5068
5347
  priv.mql = window.matchMedia(q);
@@ -5072,6 +5351,7 @@ function mt_create$20(gobj) {
5072
5351
  priv.mql.addEventListener("change", priv.mql_handler);
5073
5352
  }
5074
5353
  build_dock(gobj);
5354
+ yui_shell_watch_changes(gobj);
5075
5355
  const subscriber = gobj_read_pointer_attr(gobj, "subscriber");
5076
5356
  if (subscriber) gobj_subscribe_event(gobj, null, {}, subscriber);
5077
5357
  }
@@ -5080,6 +5360,7 @@ function mt_create$20(gobj) {
5080
5360
  ***************************************************************/
5081
5361
  function mt_destroy$20(gobj) {
5082
5362
  let priv = gobj.priv;
5363
+ yui_shell_unwatch_changes(gobj);
5083
5364
  if (priv.mql && priv.mql_handler) {
5084
5365
  priv.mql.removeEventListener("change", priv.mql_handler);
5085
5366
  priv.mql = null;
@@ -5129,10 +5410,18 @@ function ensure_dock_style() {
5129
5410
  }
5130
5411
  .yui-dock-chip {
5131
5412
  display: inline-flex; align-items: center; gap: 7px; font: inherit; font-size: 12px;
5132
- padding: 4px 5px 4px 11px; border: 1px solid var(--bulma-border); border-radius: 7px;
5133
- background: var(--bulma-scheme-main); color: var(--bulma-text); cursor: pointer; white-space: nowrap;
5413
+ padding: 0 5px 0 0; border: 1px solid var(--bulma-border); border-radius: 7px;
5414
+ background: var(--bulma-scheme-main); color: var(--bulma-text); white-space: nowrap;
5134
5415
  }
5135
5416
  .yui-dock-chip:hover { border-color: var(--bulma-text-weak); }
5417
+ /* The chip is the look; its toggle a button that adds none, and
5418
+ * takes the pill's padding, so the whole label is what a press lands
5419
+ * on and the chip reads as one pill with its ✕ at the end. */
5420
+ .yui-dock-chip-toggle {
5421
+ display: inline-flex; align-items: center; gap: 7px;
5422
+ padding: 4px 0 4px 11px; margin: 0; border: 0; border-radius: 7px 0 0 7px;
5423
+ background: transparent; font: inherit; color: inherit; cursor: pointer; white-space: nowrap;
5424
+ }
5136
5425
  /* The BORDER says active, not the text: blue ink on a blue tint is
5137
5426
  * the same hue on itself -- 4.49:1 on the light chip, under the 4.5
5138
5427
  * its 12px label needs. The label takes the text colour (11:1). */
@@ -5222,10 +5511,33 @@ function apply_placement(gobj) {
5222
5511
  "bottom-left",
5223
5512
  "bottom-right"
5224
5513
  ]) $dock.classList.toggle(`yui-dock--${c}`, !inline && c === corner);
5225
- let target = inline ? $inline : gobj_read_attr(gobj, "$parent") || document.body;
5514
+ let target = inline ? $inline : gobj_read_attr(gobj, "$parent") || dock_place(gobj);
5226
5515
  if ($dock.parentNode !== target) target.appendChild($dock);
5227
5516
  }
5228
5517
  /************************************************************
5518
+ * Where the FLOATING dock lives when the app names no
5519
+ * `$parent`: the popup layer of the shell, at the top of it.
5520
+ *
5521
+ * It was document.body at z-index 1000000, and that put it
5522
+ * above the WHOLE shell -- `.yui-shell` is a stacking context
5523
+ * of its own, so its dialogs (modal layer, 99) and toasts (120)
5524
+ * counted only inside it: the dock stayed clickable over a
5525
+ * dialog's backdrop (a window could be closed from its chip
5526
+ * while its own "all changes will be lost" question was open),
5527
+ * and it covered a toast where they met.
5528
+ *
5529
+ * The popup layer, not the window layer: the dock has to stay
5530
+ * above EVERY window, and windows live in two layers -- the
5531
+ * window layer (no `$parent`) and the popup layer, where the
5532
+ * site map and the JSON windows of the treedb views put theirs.
5533
+ * Above both; below the dialogs, the toasts and the loading
5534
+ * cover. No shell (before the first, between a logout and the
5535
+ * next login): the body.
5536
+ ************************************************************/
5537
+ function dock_place(gobj) {
5538
+ return yui_shell_popup_layer(yui_shell_of(gobj)) || document.body;
5539
+ }
5540
+ /************************************************************
5229
5541
  * Destroy the dock element.
5230
5542
  ************************************************************/
5231
5543
  function destroy_dock(gobj) {
@@ -5302,7 +5614,6 @@ function set_active(gobj, entry) {
5302
5614
  * Bring a window to front and mark it active/visible.
5303
5615
  ************************************************************/
5304
5616
  function focus_entry(gobj, entry) {
5305
- let priv = gobj.priv;
5306
5617
  let $win = gobj_read_attr(entry.gobj, "$container");
5307
5618
  if ($win) {
5308
5619
  if (entry.minimized) {
@@ -5310,8 +5621,7 @@ function focus_entry(gobj, entry) {
5310
5621
  entry.minimized = false;
5311
5622
  if (entry.$chip) entry.$chip.classList.remove("is-min");
5312
5623
  }
5313
- priv.z += 1;
5314
- $win.style.zIndex = priv.z;
5624
+ yui_shell_raise_window($win);
5315
5625
  }
5316
5626
  set_active(gobj, entry);
5317
5627
  }
@@ -5378,57 +5688,57 @@ function ac_register_window(gobj, event, kw, src) {
5378
5688
  { class: "yui-dock-label-kind" },
5379
5689
  text_node(title)
5380
5690
  ]);
5691
+ let chip_seq = ++__chip_seq__;
5692
+ let label_ids = label_items.map(function(item, i) {
5693
+ item[1].id = `yui-dock-chip-${chip_seq}-${i}`;
5694
+ return item[1].id;
5695
+ });
5381
5696
  let entry = {
5382
5697
  gobj: win,
5383
5698
  $chip: null,
5384
5699
  minimized: false
5385
5700
  };
5701
+ let window_name = gobj_full_name(win);
5386
5702
  let $chip = createElement2([
5387
5703
  "div",
5388
- Object.assign({
5389
- class: "yui-dock-chip",
5390
- role: "button",
5391
- tabindex: "0",
5392
- title
5393
- }, !prefix && title_key ? { "data-i18n-title": title_key } : {}),
5394
- [
5395
- chip_lead(kw.icon),
5396
- [
5704
+ { class: "DOCK_CHIP yui-dock-chip" },
5705
+ [[
5706
+ "button",
5707
+ Object.assign({
5708
+ class: "DOCK_CHIP_TOGGLE yui-dock-chip-toggle",
5709
+ type: "button",
5710
+ title,
5711
+ "aria-label": title,
5712
+ "aria-labelledby": label_ids.join(" ")
5713
+ }, !prefix && title_key ? {
5714
+ "data-i18n-title": title_key,
5715
+ "data-i18n-aria-label": title_key
5716
+ } : {}),
5717
+ [chip_lead(kw.icon), [
5397
5718
  "span",
5398
5719
  { class: "yui-dock-label" },
5399
5720
  label_items
5400
- ],
5401
- [
5402
- "button",
5403
- {
5404
- class: "yui-dock-close",
5405
- type: "button",
5406
- title: t("close"),
5407
- "data-i18n-title": "close",
5408
- "aria-label": t("close"),
5409
- "data-i18n-aria-label": "close"
5410
- },
5411
- html_markup(WC_X),
5412
- { click: (evt) => {
5413
- evt.stopPropagation();
5414
- gobj_send_event(entry.gobj, "EV_CLOSE_WINDOW", {}, gobj);
5415
- } }
5416
- ]
5417
- ],
5418
- {
5419
- click: (evt) => {
5721
+ ]],
5722
+ { click: (evt) => {
5420
5723
  evt.stopPropagation();
5421
- on_chip_click(gobj, entry);
5724
+ gobj_send_event(gobj, "EV_TOGGLE_WINDOW", { window: window_name }, gobj);
5725
+ } }
5726
+ ], [
5727
+ "button",
5728
+ {
5729
+ class: "DOCK_CHIP_CLOSE yui-dock-close",
5730
+ type: "button",
5731
+ title: t("close"),
5732
+ "data-i18n-title": "close",
5733
+ "aria-label": t("close"),
5734
+ "data-i18n-aria-label": "close"
5422
5735
  },
5423
- keydown: (evt) => {
5424
- if (evt.target !== evt.currentTarget) return;
5425
- if (evt.key === "Enter" || evt.key === " ") {
5426
- if (evt.key === " ") evt.preventDefault();
5427
- evt.stopPropagation();
5428
- on_chip_click(gobj, entry);
5429
- }
5430
- }
5431
- }
5736
+ html_markup(WC_X),
5737
+ { click: (evt) => {
5738
+ evt.stopPropagation();
5739
+ gobj_send_event(entry.gobj, "EV_CLOSE_WINDOW", {}, gobj);
5740
+ } }
5741
+ ]]
5432
5742
  ]);
5433
5743
  entry.$chip = $chip;
5434
5744
  priv.windows.push(entry);
@@ -5472,6 +5782,41 @@ function ac_focus_window(gobj, event, kw, src) {
5472
5782
  if (entry) focus_entry(gobj, entry);
5473
5783
  return 0;
5474
5784
  }
5785
+ /************************************************************
5786
+ * EV_TOGGLE_WINDOW { window } -- the toggle of a chip of THIS
5787
+ * dock was pressed: bring a minimized or covered window to the
5788
+ * front, send the one in front to the dock. Only the dock's own
5789
+ * chip sends it (src is the dock): the events a window sends
5790
+ * about itself come from the window, and this one from the chip
5791
+ * that names it.
5792
+ ************************************************************/
5793
+ function ac_toggle_window(gobj, event, kw, src) {
5794
+ let priv = gobj.priv;
5795
+ if (src !== gobj) {
5796
+ log_error(`${gobj_short_name(gobj)}: ${event} comes from the dock's own chip, not from ${is_gobj(src) ? gobj_full_name(src) : String(src)}`);
5797
+ return -1;
5798
+ }
5799
+ let name = kw && kw.window;
5800
+ let entry = priv.windows.find((e) => gobj_full_name(e.gobj) === name);
5801
+ if (!entry) {
5802
+ log_error(`${gobj_short_name(gobj)}: ${event}: no window '${name}' in the dock`);
5803
+ return -1;
5804
+ }
5805
+ on_chip_click(gobj, entry);
5806
+ return 0;
5807
+ }
5808
+ /************************************************************
5809
+ * EV_SHELL_CHANGED -- a shell was built, or is being destroyed
5810
+ * (yui_shell_watch_changes()). The dock's place -- the shell's
5811
+ * popup layer, or its zone when inline -- may have gone with
5812
+ * it, or be in the new one: place it again. Up to 7.26.34 an
5813
+ * inline dock stayed in the destroyed shell's zone after a
5814
+ * logout, detached, until a window registered.
5815
+ ************************************************************/
5816
+ function ac_shell_changed(gobj, event, kw, src) {
5817
+ apply_placement(gobj);
5818
+ return 0;
5819
+ }
5475
5820
  /***************************
5476
5821
  * FSM
5477
5822
  ***************************/
@@ -5491,7 +5836,9 @@ function create_gclass$20(gclass_name) {
5491
5836
  ["EV_REGISTER_WINDOW", 0],
5492
5837
  ["EV_UNREGISTER_WINDOW", 0],
5493
5838
  ["EV_MINIMIZE_WINDOW", 0],
5494
- ["EV_FOCUS_WINDOW", 0]
5839
+ ["EV_FOCUS_WINDOW", 0],
5840
+ ["EV_TOGGLE_WINDOW", 0],
5841
+ ["EV_SHELL_CHANGED", 0]
5495
5842
  ], [["ST_IDLE", [
5496
5843
  [
5497
5844
  "EV_REGISTER_WINDOW",
@@ -5512,6 +5859,16 @@ function create_gclass$20(gclass_name) {
5512
5859
  "EV_FOCUS_WINDOW",
5513
5860
  ac_focus_window,
5514
5861
  null
5862
+ ],
5863
+ [
5864
+ "EV_TOGGLE_WINDOW",
5865
+ ac_toggle_window,
5866
+ null
5867
+ ],
5868
+ [
5869
+ "EV_SHELL_CHANGED",
5870
+ ac_shell_changed,
5871
+ null
5515
5872
  ]
5516
5873
  ]]], gmt$20, 0, attrs_table$20, PRIVATE_DATA$20, 0, 0, 0, 0);
5517
5874
  if (!__gclass__$20) return -1;
@@ -6791,6 +7148,41 @@ function yui_tabulator_name_row_selects(table, t) {
6791
7148
  }
6792
7149
  } catch (e) {}
6793
7150
  }
7151
+ /***************************************************************
7152
+ * Name the EDITOR of an editable column: the box Tabulator draws
7153
+ * in a cell while it is edited (input, number, textarea, list,
7154
+ * tickCross, date...).
7155
+ *
7156
+ * Tabulator gives it nothing -- no label, no aria-label, no title
7157
+ * -- so a reader lands in an anonymous edit box. Its name is the
7158
+ * column's: a cell is edited under its column's title. Asked at
7159
+ * the MOMENT of editing: Tabulator takes `editorParams` as a
7160
+ * function of the cell, and every built-in editor writes the
7161
+ * `elementAttributes` it answers onto the element it draws. So
7162
+ * the name is in the language of that moment, with nothing to
7163
+ * re-apply after a language change or a rebuild of the columns
7164
+ * -- the twin of the header filters above, which have to be
7165
+ * renamed after every render.
7166
+ *
7167
+ * `name_of(cell)` answers the column's title, TRANSLATED (the
7168
+ * view's own label: a cascade, a schema header); "" names
7169
+ * nothing. What `editorParams` the column had is kept, its own
7170
+ * `elementAttributes` too; a name it set itself wins.
7171
+ ***************************************************************/
7172
+ function yui_tabulator_name_editor(column_def, name_of) {
7173
+ if (!column_def || !column_def.editor || typeof name_of !== "function") return;
7174
+ let base = column_def.editorParams;
7175
+ column_def.editorParams = function(cell) {
7176
+ let params = typeof base === "function" ? base(cell) : base || {};
7177
+ let name = name_of(cell);
7178
+ if (!name) return params;
7179
+ let attrs = Object.assign({
7180
+ "aria-label": name,
7181
+ title: name
7182
+ }, params.elementAttributes || {});
7183
+ return Object.assign({}, params, { elementAttributes: attrs });
7184
+ };
7185
+ }
6794
7186
  function yui_tabulator_relocalize(table, t) {
6795
7187
  if (!table) return;
6796
7188
  try {
@@ -7311,6 +7703,7 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
7311
7703
  {
7312
7704
  type: "button",
7313
7705
  class: "FILE_PICK button is-small",
7706
+ title: t("choose a file"),
7314
7707
  "data-i18n-title": "choose a file",
7315
7708
  "aria-label": t("choose a file"),
7316
7709
  "data-i18n-aria-label": "choose a file"
@@ -7329,6 +7722,7 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
7329
7722
  {
7330
7723
  type: "button",
7331
7724
  class: "FILE_CLEAR button is-small",
7725
+ title: t("remove the file"),
7332
7726
  "data-i18n-title": "remove the file",
7333
7727
  "aria-label": t("remove the file"),
7334
7728
  "data-i18n-aria-label": "remove the file"
@@ -8677,6 +9071,293 @@ function json_editor_value(content) {
8677
9071
  return JSON.stringify(content.json);
8678
9072
  }
8679
9073
  //#endregion
9074
+ //#region src/form_json_editor_names.js
9075
+ /***********************************************************************
9076
+ * form_json_editor_names.js
9077
+ *
9078
+ * The NAMES of the buttons vanilla-jsoneditor draws in its menu
9079
+ * (a json field of C_YUI_FORM): in the app's language, and with an
9080
+ * aria-label.
9081
+ *
9082
+ * The editor draws them itself, in English, with a `title` and
9083
+ * nothing else: "Format JSON: add proper indentation and new lines
9084
+ * (Ctrl+I)", "Undo (Ctrl+Z)", and three mode buttons whose text is
9085
+ * "text", "tree" and "table" (REV-CLOUD V23.5.6). Two hooks reach
9086
+ * them, and both are needed:
9087
+ *
9088
+ * - json_editor_menu_items(), the editor's `onRenderMenu`: the
9089
+ * items it is about to draw, with each title translated (its
9090
+ * shortcut kept) and the mode buttons' text too. Svelte draws
9091
+ * from these, so a button born in a re-render is born named.
9092
+ * - name_json_editor_menu(), after the render, from the form's
9093
+ * FSM: the aria-label of each button, with its key, so
9094
+ * refresh_language() re-translates it, and the state of the
9095
+ * mode buttons (aria-pressed). A menu item has no field for
9096
+ * either.
9097
+ *
9098
+ * A button is known by the CLASS the editor gives it (`jse-format`,
9099
+ * `jse-undo`...), never by its English title: a class is what the
9100
+ * editor's own stylesheet relies on. A button not listed here keeps
9101
+ * the editor's words.
9102
+ *
9103
+ * Its CONTEXT MENU (`onRenderContextMenu`,
9104
+ * json_editor_context_menu_items()) has no class on its items, so
9105
+ * an item is known by what it IS and where it IS: the icon the
9106
+ * editor draws it with (a FontAwesome name: `paste`, `scissors`),
9107
+ * the column of the menu it stands in, its place in a dropdown
9108
+ * (the main button, the first or the second of its list), the
9109
+ * mode of the editor (tree or table) -- never by its words. Its
9110
+ * text and title go back translated; after the render the same
9111
+ * pass names its buttons, and the arrow of each dropdown, which
9112
+ * the editor draws with no name at all.
9113
+ *
9114
+ * Not reached, because the editor gives them no hook: its search
9115
+ * box, its repair messages, the tip at the foot of the context
9116
+ * menu, and its modals (sort, transform). They stay in its
9117
+ * English.
9118
+ *
9119
+ * Copyright (c) 2026, ArtGins.
9120
+ * All Rights Reserved.
9121
+ ***********************************************************************/
9122
+ /***************************************************************
9123
+ * Constants
9124
+ ***************************************************************/
9125
+ var JSON_EDITOR_BUTTON_KEYS = {
9126
+ "jse-expand-all": "expand all",
9127
+ "jse-collapse-all": "collapse all",
9128
+ "jse-search": "search",
9129
+ "jse-copy": "copy",
9130
+ "jse-undo": "undo",
9131
+ "jse-redo": "redo",
9132
+ "jse-cancel": "cancel",
9133
+ "jse-format": "format json",
9134
+ "jse-compact": "compact json",
9135
+ "jse-sort": "sort",
9136
+ "jse-transform": "transform",
9137
+ "jse-contextmenu": "open context menu"
9138
+ };
9139
+ var JSON_EDITOR_MODE_NAME_KEYS = {
9140
+ text: "text view",
9141
+ tree: "tree view",
9142
+ table: "table view"
9143
+ };
9144
+ var JSON_EDITOR_MODE_TEXT_KEYS = {
9145
+ text: "text mode",
9146
+ tree: "tree mode",
9147
+ table: "table mode"
9148
+ };
9149
+ var MODE_CLASS = "yui-jse-mode-";
9150
+ var SHORTCUT = /\((?:Ctrl|Cmd|⌘)[^()]*\)\s*$/;
9151
+ var CONTEXT_BUTTON_KEYS = {
9152
+ "pen": "edit key",
9153
+ "paste": "paste",
9154
+ "clone": "duplicate",
9155
+ "crop-simple": "extract",
9156
+ "arrow-down-short-wide": "sort",
9157
+ "filter": "transform",
9158
+ "trash-can": "remove",
9159
+ "square-caret-up": "insert before",
9160
+ "square-caret-down": "insert after"
9161
+ };
9162
+ var CONTEXT_ROW_BUTTON_KEYS = {
9163
+ "pen": "edit row",
9164
+ "clone": "duplicate row",
9165
+ "trash-can": "remove row"
9166
+ };
9167
+ var CONTEXT_DROPDOWN_MAIN_KEYS = {
9168
+ "pen": "edit value",
9169
+ "scissors": "cut",
9170
+ "copy": "copy"
9171
+ };
9172
+ var CONTEXT_DROPDOWN_FIRST_KEYS = {
9173
+ "pen": "edit value",
9174
+ "scissors": "cut formatted",
9175
+ "copy": "copy formatted"
9176
+ };
9177
+ var CONTEXT_DROPDOWN_SECOND_KEYS = {
9178
+ "pen": "enforce string",
9179
+ "scissors": "cut compacted",
9180
+ "copy": "copy compacted"
9181
+ };
9182
+ var CONTEXT_TARGET_KEYS = {
9183
+ "0": "structure",
9184
+ "1": "object",
9185
+ "2": "array",
9186
+ "3": "value"
9187
+ };
9188
+ var TARGET_ICONS = ["arrow-right-arrow-left", "plus"];
9189
+ var CONTEXT_LABEL_KEYS = {
9190
+ convert: "convert to",
9191
+ insert: "insert",
9192
+ cell: "table cell",
9193
+ row: "table row"
9194
+ };
9195
+ /***************************
9196
+ * Local Methods
9197
+ ***************************/
9198
+ function has_key(table, name) {
9199
+ return Object.prototype.hasOwnProperty.call(table, name);
9200
+ }
9201
+ function button_class(classes) {
9202
+ return (Array.isArray(classes) ? classes : String(classes || "").split(/\s+/)).find((c) => has_key(JSON_EDITOR_BUTTON_KEYS, c)) || "";
9203
+ }
9204
+ function mode_of_item(item) {
9205
+ if (String(item.className || "").split(/\s+/).indexOf("jse-group-button") < 0) return "";
9206
+ return has_key(JSON_EDITOR_MODE_NAME_KEYS, item.text) ? item.text : "";
9207
+ }
9208
+ function icon_of(item) {
9209
+ return item && item.icon && typeof item.icon.iconName === "string" ? item.icon.iconName : "";
9210
+ }
9211
+ function named_item(item, name) {
9212
+ let m = SHORTCUT.exec(String(item.title || ""));
9213
+ return Object.assign({}, item, {
9214
+ text: name,
9215
+ title: m ? `${name} ${m[0].trim()}` : name
9216
+ });
9217
+ }
9218
+ function context_dropdown(item) {
9219
+ let icon = icon_of(item.main);
9220
+ if (!has_key(CONTEXT_DROPDOWN_MAIN_KEYS, icon)) return item;
9221
+ let list = Array.isArray(item.items) ? item.items : [];
9222
+ return Object.assign({}, item, {
9223
+ main: named_item(item.main, t(CONTEXT_DROPDOWN_MAIN_KEYS[icon])),
9224
+ items: list.map(function(it, i) {
9225
+ if (!it || it.type !== "button") return it;
9226
+ if (i === 0) return named_item(it, t(CONTEXT_DROPDOWN_FIRST_KEYS[icon]));
9227
+ if (i === 1) return named_item(it, t(CONTEXT_DROPDOWN_SECOND_KEYS[icon]));
9228
+ return it;
9229
+ })
9230
+ });
9231
+ }
9232
+ function context_button(item, row_column) {
9233
+ let icon = icon_of(item);
9234
+ if (row_column && has_key(CONTEXT_ROW_BUTTON_KEYS, icon)) return named_item(item, t(CONTEXT_ROW_BUTTON_KEYS[icon]));
9235
+ if (has_key(CONTEXT_BUTTON_KEYS, icon)) return named_item(item, t(CONTEXT_BUTTON_KEYS[icon]));
9236
+ return item;
9237
+ }
9238
+ function context_column(item, table, index) {
9239
+ let list = Array.isArray(item.items) ? item.items : [];
9240
+ let buttons = list.filter((it) => it && it.type === "button");
9241
+ let targets = !table && buttons.length > 0 && buttons.every((it) => TARGET_ICONS.indexOf(icon_of(it)) >= 0);
9242
+ let row_column = table && index === 1;
9243
+ let label_kind = "";
9244
+ if (table) label_kind = index === 0 ? "cell" : index === 1 ? "row" : "";
9245
+ else if (targets) label_kind = icon_of(buttons[0]) === "plus" ? "insert" : "convert";
9246
+ let target = 0;
9247
+ let insert_row = 0;
9248
+ return Object.assign({}, item, { items: list.map(function(it) {
9249
+ if (!it || typeof it !== "object") return it;
9250
+ if (it.type === "label") return label_kind ? Object.assign({}, it, { text: t(CONTEXT_LABEL_KEYS[label_kind]) }) : it;
9251
+ if (it.type === "dropdown-button") return context_dropdown(it);
9252
+ if (it.type !== "button") return it;
9253
+ if (targets) return named_item(it, t(CONTEXT_TARGET_KEYS[String(target++)]));
9254
+ if (row_column && icon_of(it) === "plus") return named_item(it, t(insert_row++ === 0 ? CONTEXT_BUTTON_KEYS["square-caret-up"] : CONTEXT_BUTTON_KEYS["square-caret-down"]));
9255
+ return context_button(it, row_column);
9256
+ }) });
9257
+ }
9258
+ function context_item(item, table) {
9259
+ if (!item || typeof item !== "object") return item;
9260
+ if (item.type === "row") {
9261
+ let column = 0;
9262
+ let list = Array.isArray(item.items) ? item.items : [];
9263
+ return Object.assign({}, item, { items: list.map(function(it) {
9264
+ if (it && it.type === "column") return context_column(it, table, column++);
9265
+ return context_item(it, table);
9266
+ }) });
9267
+ }
9268
+ if (item.type === "dropdown-button") return context_dropdown(item);
9269
+ if (item.type === "button") return context_button(item, false);
9270
+ return item;
9271
+ }
9272
+ function mode_of_button($b) {
9273
+ for (let c of $b.classList) if (c.indexOf(MODE_CLASS) === 0) {
9274
+ let mode = c.slice(13);
9275
+ if (has_key(JSON_EDITOR_MODE_NAME_KEYS, mode)) return mode;
9276
+ }
9277
+ return "";
9278
+ }
9279
+ /***************************
9280
+ * Public
9281
+ ***************************/
9282
+ /************************************************************
9283
+ * The editor's `onRenderMenu`: the items it is about to draw,
9284
+ * translated. A title keeps the editor's shortcut after the
9285
+ * translated name ("Deshacer (Ctrl+Z)"); a mode button gets its
9286
+ * mode's name as title and its short text translated, and a class
9287
+ * that says its mode. Anything else is returned as it came.
9288
+ *
9289
+ * Pure: what it answers is data for the editor to draw.
9290
+ ************************************************************/
9291
+ function json_editor_menu_items(items) {
9292
+ if (!Array.isArray(items)) return items;
9293
+ return items.map(function(item) {
9294
+ if (!item || item.type !== "button") return item;
9295
+ let mode = mode_of_item(item);
9296
+ if (mode) return Object.assign({}, item, {
9297
+ className: `${item.className} ${MODE_CLASS}${mode}`,
9298
+ title: t(JSON_EDITOR_MODE_NAME_KEYS[mode]),
9299
+ text: t(JSON_EDITOR_MODE_TEXT_KEYS[mode])
9300
+ });
9301
+ let cls = button_class(item.className);
9302
+ if (!cls) return item;
9303
+ let m = SHORTCUT.exec(String(item.title || ""));
9304
+ let name = t(JSON_EDITOR_BUTTON_KEYS[cls]);
9305
+ return Object.assign({}, item, { title: m ? `${name} ${m[0].trim()}` : name });
9306
+ });
9307
+ }
9308
+ /************************************************************
9309
+ * The editor's `onRenderContextMenu`: the items it is about to
9310
+ * draw, translated -- each known by its icon and its place, never
9311
+ * by its words (context_item()). Anything the module does not
9312
+ * know comes back as it came: a newer editor that moves an item
9313
+ * leaves it in English, it does not mislabel it.
9314
+ *
9315
+ * Pure: what it answers is data for the editor to draw.
9316
+ ************************************************************/
9317
+ function json_editor_context_menu_items(items, context) {
9318
+ if (!Array.isArray(items)) return items;
9319
+ let table = !!(context && context.mode === "table");
9320
+ return items.map((item) => context_item(item, table));
9321
+ }
9322
+ /************************************************************
9323
+ * Name the buttons of a rendered editor's menu: an aria-label
9324
+ * with its key (data-i18n-aria-label, so a language change
9325
+ * reaches it), and aria-pressed on the mode buttons -- the
9326
+ * selected one is the mode on screen (`jse-selected`). Run
9327
+ * after every render of the menu: a re-render may draw new
9328
+ * buttons, which carry none of it.
9329
+ ************************************************************/
9330
+ function name_json_editor_menu($editor) {
9331
+ if (!$editor || typeof $editor.querySelectorAll !== "function") return 0;
9332
+ let named = 0;
9333
+ for (let $b of $editor.querySelectorAll(".jse-menu button")) {
9334
+ let mode = mode_of_button($b);
9335
+ const key = mode ? JSON_EDITOR_MODE_NAME_KEYS[mode] : JSON_EDITOR_BUTTON_KEYS[button_class([...$b.classList])];
9336
+ if (!key) continue;
9337
+ $b.setAttribute("aria-label", t(key));
9338
+ $b.setAttribute("data-i18n-aria-label", key);
9339
+ if (mode) $b.setAttribute("aria-pressed", $b.classList.contains("jse-selected") ? "true" : "false");
9340
+ named++;
9341
+ }
9342
+ const MORE = "more options";
9343
+ for (let $b of $editor.querySelectorAll("div.jse-contextmenu button")) {
9344
+ if ($b.getAttribute("data-type") === "jse-open-dropdown") {
9345
+ $b.setAttribute("title", t(MORE));
9346
+ $b.setAttribute("data-i18n-title", MORE);
9347
+ $b.setAttribute("aria-label", t(MORE));
9348
+ $b.setAttribute("data-i18n-aria-label", MORE);
9349
+ named++;
9350
+ continue;
9351
+ }
9352
+ let text = ($b.textContent || "").trim();
9353
+ if (text) {
9354
+ $b.setAttribute("aria-label", text);
9355
+ named++;
9356
+ }
9357
+ }
9358
+ return named;
9359
+ }
9360
+ //#endregion
8680
9361
  //#region src/c_yui_form.js
8681
9362
  /***********************************************************************
8682
9363
  * c_yui_form.js
@@ -8726,7 +9407,9 @@ var attrs_table$17 = [
8726
9407
  var PRIVATE_DATA$17 = {
8727
9408
  rearm_timer: null,
8728
9409
  tables: null,
8729
- table_seq: 0
9410
+ table_seq: 0,
9411
+ json_names_posted: false,
9412
+ lang_shell: null
8730
9413
  };
8731
9414
  var __gclass__$17 = null;
8732
9415
  /***************************
@@ -8757,13 +9440,23 @@ function mt_destroy$17(gobj) {
8757
9440
  * Framework Method start
8758
9441
  ************************************************************/
8759
9442
  function mt_start$17(gobj) {
9443
+ let priv = gobj.priv;
9444
+ let shell = yui_shell_of(gobj);
9445
+ if (shell) {
9446
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
9447
+ priv.lang_shell = shell;
9448
+ }
8760
9449
  let record = gobj_read_attr(gobj, "record");
8761
9450
  if (record) gobj_send_event(gobj, "EV_LOAD_RECORD", record, gobj);
8762
9451
  }
8763
9452
  /************************************************************
8764
9453
  * Framework Method stop
8765
9454
  ************************************************************/
8766
- function mt_stop$17(gobj) {}
9455
+ function mt_stop$17(gobj) {
9456
+ let priv = gobj.priv;
9457
+ if (priv.lang_shell && !gobj_is_destroying(priv.lang_shell)) gobj_unsubscribe_event(priv.lang_shell, "EV_LANGUAGE_CHANGED", {}, gobj);
9458
+ priv.lang_shell = null;
9459
+ }
8767
9460
  /***************************
8768
9461
  * Local Methods
8769
9462
  ***************************/
@@ -9219,6 +9912,80 @@ function name_form_control$1($field, conf) {
9219
9912
  $clear.setAttribute("data-i18n-aria-label", CLEAR_KEY);
9220
9913
  }
9221
9914
  }
9915
+ /************************************************************
9916
+ * The ✕ of ONE selected option of a Tom Select: its
9917
+ * remove_button plugin calls this for every option it draws.
9918
+ *
9919
+ * The plugin's own was a div titled "Remove" in English -- never
9920
+ * translated, no aria-label -- with tabindex -1, and the form
9921
+ * turns Backspace deletion off (no_backspace_delete): a keyboard
9922
+ * could not take one option out at all. Named here through t()
9923
+ * with its key (so a language change reaches it), and pressed by
9924
+ * Enter or Space as the clear button is: the key clicks it, which
9925
+ * runs the plugin's own handler.
9926
+ ************************************************************/
9927
+ function remove_option_button() {
9928
+ const KEY = "remove this option";
9929
+ let $b = createElement2([
9930
+ "div",
9931
+ {
9932
+ class: "remove",
9933
+ role: "button",
9934
+ tabindex: "0",
9935
+ title: t(KEY),
9936
+ "data-i18n-title": KEY,
9937
+ "aria-label": t(KEY),
9938
+ "data-i18n-aria-label": KEY
9939
+ },
9940
+ "×"
9941
+ ]);
9942
+ $b.addEventListener("keydown", function(evt) {
9943
+ if (evt.key === "Enter" || evt.key === " ") {
9944
+ evt.preventDefault();
9945
+ evt.stopPropagation();
9946
+ $b.click();
9947
+ }
9948
+ });
9949
+ return $b;
9950
+ }
9951
+ /************************************************************
9952
+ * The `onRenderMenu` of a json editor of this form.
9953
+ *
9954
+ * The editor calls it with the items it is about to draw: they go
9955
+ * back translated (json_editor_menu_items(), data for the editor
9956
+ * to draw). What no item can carry -- an aria-label, the state of
9957
+ * the mode buttons -- is written on the drawn buttons, after the
9958
+ * render, by the form's FSM (EV_NAME_JSON_EDITORS): the callback
9959
+ * only posts the event, once until it is handled. Posted, not
9960
+ * sent: when it runs the editor has not drawn the menu yet.
9961
+ *
9962
+ * A new function each call: handing an editor a new one is what
9963
+ * makes it draw its menu again (ac_language_changed).
9964
+ ************************************************************/
9965
+ function json_editor_render_menu(gobj) {
9966
+ return function(items) {
9967
+ ask_json_editor_names(gobj);
9968
+ return json_editor_menu_items(items);
9969
+ };
9970
+ }
9971
+ /************************************************************
9972
+ * The `onRenderContextMenu` of a json editor of this form: the
9973
+ * same, for the menu it opens on a right click or Ctrl+Q. It is
9974
+ * built each time it opens, in the language of that moment.
9975
+ ************************************************************/
9976
+ function json_editor_render_context_menu(gobj) {
9977
+ return function(items, context) {
9978
+ ask_json_editor_names(gobj);
9979
+ return json_editor_context_menu_items(items, context);
9980
+ };
9981
+ }
9982
+ function ask_json_editor_names(gobj) {
9983
+ let priv = gobj.priv;
9984
+ if (!priv.json_names_posted && !gobj_is_destroying(gobj)) {
9985
+ priv.json_names_posted = true;
9986
+ gobj_post_event(gobj, "EV_NAME_JSON_EDITORS", {}, gobj);
9987
+ }
9988
+ }
9222
9989
  function build_form(gobj) {
9223
9990
  let form_id = gobj_read_str_attr(gobj, "form_id");
9224
9991
  let $form = createElement2(["form", {
@@ -9250,7 +10017,7 @@ function build_html_form(gobj, $form, prefix, template) {
9250
10017
  let label_keys = topic_name ? [topic_name + "." + field_desc.name, field_desc.name] : [field_desc.name];
9251
10018
  const NO_I18N = "\0";
9252
10019
  html_field_conf.label = t(label_keys, { defaultValue: field_desc.header });
9253
- html_field_conf.label_i18n = t(field_desc.name, { defaultValue: NO_I18N }) !== NO_I18N ? field_desc.name : null;
10020
+ html_field_conf.label_i18n = label_keys.find((k) => t(k, { defaultValue: NO_I18N }) !== NO_I18N) || null;
9254
10021
  html_field_conf.placeholder = field_desc.placeholder ? t(field_desc.placeholder) : "";
9255
10022
  let placeholders = gobj_read_attr(gobj, "placeholders");
9256
10023
  if (placeholders[key]) html_field_conf.placeholder = placeholders[key];
@@ -9956,7 +10723,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
9956
10723
  "clear_button": { "title": t("remove all selected options") },
9957
10724
  "no_active_items": {},
9958
10725
  "no_backspace_delete": {},
9959
- "remove_button": {}
10726
+ "remove_button": { html: remove_option_button }
9960
10727
  }
9961
10728
  });
9962
10729
  $extend.tom_select.on("change", function() {
@@ -10025,7 +10792,9 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
10025
10792
  },
10026
10793
  timestampTag: function({ field, value, path }) {
10027
10794
  return is_time_field(field);
10028
- }
10795
+ },
10796
+ onRenderMenu: json_editor_render_menu(gobj),
10797
+ onRenderContextMenu: json_editor_render_context_menu(gobj)
10029
10798
  }
10030
10799
  });
10031
10800
  break;
@@ -10410,6 +11179,7 @@ function template2columns(gobj, columns, template, sub_elements) {
10410
11179
  else return value;
10411
11180
  };
10412
11181
  if (field_desc.is_required) validator.push("required");
11182
+ if (is_object(value) && value.validator) validator.push(...Array.isArray(value.validator) ? value.validator : [value.validator]);
10413
11183
  switch (field_desc.type) {
10414
11184
  case "object":
10415
11185
  case "dict": break;
@@ -10556,6 +11326,9 @@ function template2columns(gobj, columns, template, sub_elements) {
10556
11326
  }
10557
11327
  if (column) {
10558
11328
  if (readonly) delete column.editor;
11329
+ yui_tabulator_name_editor(column, function() {
11330
+ return t(field_desc.header || field_desc.name);
11331
+ });
10559
11332
  columns.push(column);
10560
11333
  }
10561
11334
  });
@@ -10626,12 +11399,20 @@ function validate_form(gobj, $form) {
10626
11399
  mark_field_validity($editor);
10627
11400
  }
10628
11401
  });
11402
+ let $first_bad = null;
11403
+ function table_valid(tabulator) {
11404
+ let r = tabulator.validate();
11405
+ if (r === true) return true;
11406
+ if (!$first_bad && Array.isArray(r) && r.length > 0) $first_bad = r[0].getElement();
11407
+ return false;
11408
+ }
10629
11409
  $form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
10630
- if (!$table.tabulator.validate()) validated = false;
11410
+ if (!table_valid($table.tabulator)) validated = false;
10631
11411
  $table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
10632
- if (!$table_nested.tabulator.validate()) validated = false;
11412
+ if (!table_valid($table_nested.tabulator)) validated = false;
10633
11413
  });
10634
11414
  });
11415
+ if ($first_bad) $first_bad.scrollIntoView({ block: "nearest" });
10635
11416
  return validated;
10636
11417
  }
10637
11418
  /************************************************************
@@ -10751,7 +11532,13 @@ function clear_data(gobj, $form) {
10751
11532
  } else if ($input.type === "checkbox") $input.checked = false;
10752
11533
  else if ($input.type === "radio") $input.checked = false;
10753
11534
  else if ($input.type === "datetime-local") $input.value = null;
10754
- else if ($input.type === "select-multiple") $input.tom_select.clear();
11535
+ else if ($input.type === "color") {
11536
+ $input.value = "#000000";
11537
+ if ($input.yui_color_value) {
11538
+ $input.yui_color_value.textContent = $input.value;
11539
+ $input.yui_color_value.title = $input.value;
11540
+ }
11541
+ } else if ($input.type === "select-multiple") $input.tom_select.clear();
10755
11542
  else {
10756
11543
  $input.value = null;
10757
11544
  if ($input.yui_svg_preview) $input.yui_svg_preview();
@@ -11302,6 +12089,39 @@ function ac_hide$11(gobj, event, kw, src) {
11302
12089
  function ac_timeout(gobj, event, kw, src) {
11303
12090
  return 0;
11304
12091
  }
12092
+ /************************************************************
12093
+ * EV_NAME_JSON_EDITORS {} -- a menu of a json editor of this
12094
+ * form was drawn, its menu or its context menu (posted by
12095
+ * ask_json_editor_names): name its buttons. Every editor of the
12096
+ * form: naming is idempotent, and a kw is plain json, which an
12097
+ * editor is not.
12098
+ ************************************************************/
12099
+ function ac_name_json_editors(gobj, event, kw, src) {
12100
+ let priv = gobj.priv;
12101
+ priv.json_names_posted = false;
12102
+ let $container = gobj_read_attr(gobj, "$container");
12103
+ if (!$container) {
12104
+ log_error(`${gobj_short_name(gobj)}: ${event} without a container`);
12105
+ return -1;
12106
+ }
12107
+ for (let $editor of $container.querySelectorAll(".jsoneditor")) name_json_editor_menu($editor);
12108
+ return 0;
12109
+ }
12110
+ /************************************************************
12111
+ * EV_LANGUAGE_CHANGED {} -- from the shell. A json editor draws
12112
+ * its menu once: its titles and the text of its mode buttons stay
12113
+ * in the old language until it draws it again, so each is told to
12114
+ * (a new onRenderMenu), and the render names it again.
12115
+ ************************************************************/
12116
+ function ac_language_changed$10(gobj, event, kw, src) {
12117
+ let $container = gobj_read_attr(gobj, "$container");
12118
+ if (!$container) return 0;
12119
+ for (let $editor of $container.querySelectorAll(".jsoneditor")) {
12120
+ let editor = $editor.jsoneditor;
12121
+ if (editor && typeof editor.updateProps === "function") editor.updateProps({ onRenderMenu: json_editor_render_menu(gobj) });
12122
+ }
12123
+ return 0;
12124
+ }
11305
12125
  /***************************
11306
12126
  * FSM
11307
12127
  ***************************/
@@ -11451,6 +12271,16 @@ function create_gclass$17(gclass_name) {
11451
12271
  "EV_TIMEOUT",
11452
12272
  ac_timeout,
11453
12273
  null
12274
+ ],
12275
+ [
12276
+ "EV_NAME_JSON_EDITORS",
12277
+ ac_name_json_editors,
12278
+ null
12279
+ ],
12280
+ [
12281
+ "EV_LANGUAGE_CHANGED",
12282
+ ac_language_changed$10,
12283
+ null
11454
12284
  ]
11455
12285
  ]]];
11456
12286
  const event_types = [
@@ -11469,7 +12299,9 @@ function create_gclass$17(gclass_name) {
11469
12299
  ["EV_REFRESH", 0],
11470
12300
  ["EV_SHOW", 0],
11471
12301
  ["EV_HIDE", 0],
11472
- ["EV_TIMEOUT", 0]
12302
+ ["EV_TIMEOUT", 0],
12303
+ ["EV_NAME_JSON_EDITORS", 0],
12304
+ ["EV_LANGUAGE_CHANGED", 0]
11473
12305
  ];
11474
12306
  __gclass__$17 = gclass_create(gclass_name, event_types, states, gmt$17, 0, attrs_table$17, PRIVATE_DATA$17, 0, 0, 0, 0);
11475
12307
  if (!__gclass__$17) return -1;
@@ -11489,7 +12321,7 @@ function register_c_yui_form() {
11489
12321
  *
11490
12322
  * Utilities for maplibre
11491
12323
  *
11492
- * Copyright (c) 2025, ArtGins.
12324
+ * Copyright (c) 2025-2026, ArtGins.
11493
12325
  * All Rights Reserved.
11494
12326
  ***********************************************************************/
11495
12327
  var EditControl = class {
@@ -11685,9 +12517,14 @@ function label_ctrl($root, selector, text) {
11685
12517
  $el.setAttribute("aria-label", text);
11686
12518
  }
11687
12519
  }
12520
+ function relabel_written($root, selector, was, now) {
12521
+ if (!$root || !now) return;
12522
+ for (const $el of $root.querySelectorAll(selector)) if ($el.getAttribute("aria-label") === was) $el.setAttribute("aria-label", now);
12523
+ }
11688
12524
  function yui_maplibre_relocalize(map, t) {
11689
12525
  if (!map || typeof map.getContainer !== "function") return;
11690
12526
  const strings = yui_maplibre_locale(t);
12527
+ const was = Object.assign({}, map._locale);
11691
12528
  try {
11692
12529
  map._locale = Object.assign({}, map._locale, strings);
11693
12530
  } catch (e) {}
@@ -11699,6 +12536,8 @@ function yui_maplibre_relocalize(map, t) {
11699
12536
  label_ctrl($c, ".maplibregl-ctrl-attrib-button", strings["AttributionControl.ToggleAttribution"]);
11700
12537
  label_ctrl($c, ".maplibregl-ctrl-fullscreen", strings["FullscreenControl.Enter"]);
11701
12538
  label_ctrl($c, ".maplibregl-popup-close-button", strings["Popup.Close"]);
12539
+ relabel_written($c, ".maplibregl-canvas", was["Map.Title"], strings["Map.Title"]);
12540
+ relabel_written($c, ".maplibregl-marker", was["Marker.Title"], strings["Marker.Title"]);
11702
12541
  const mac = typeof navigator !== "undefined" && (navigator.userAgent || "").includes("Mac");
11703
12542
  const $desktop = $c.querySelector(".maplibregl-desktop-message");
11704
12543
  if ($desktop) $desktop.textContent = mac ? strings["CooperativeGesturesHandler.MacHelpText"] : strings["CooperativeGesturesHandler.WindowsHelpText"];
@@ -14447,6 +15286,8 @@ function make_toolbar$5(gobj) {
14447
15286
  type: "text",
14448
15287
  placeholder: t("search"),
14449
15288
  "data-i18n-placeholder": "search",
15289
+ title: t("search"),
15290
+ "data-i18n-title": "search",
14450
15291
  "aria-label": t("search"),
14451
15292
  "data-i18n-aria-label": "search"
14452
15293
  },
@@ -15987,6 +16828,8 @@ function make_toolbar$4(gobj) {
15987
16828
  type: "text",
15988
16829
  placeholder: t("search"),
15989
16830
  "data-i18n-placeholder": "search",
16831
+ title: t("search"),
16832
+ "data-i18n-title": "search",
15990
16833
  "aria-label": t("search"),
15991
16834
  "data-i18n-aria-label": "search"
15992
16835
  },
@@ -19260,6 +20103,8 @@ function make_toolbar$2(gobj) {
19260
20103
  type: "text",
19261
20104
  placeholder: t("search"),
19262
20105
  "data-i18n-placeholder": "search",
20106
+ title: t("search"),
20107
+ "data-i18n-title": "search",
19263
20108
  "aria-label": t("search"),
19264
20109
  "data-i18n-aria-label": "search"
19265
20110
  },
@@ -21080,7 +21925,7 @@ function open_gclass_view(host, gclass_name, opts) {
21080
21925
  gobj_start(jv);
21081
21926
  return handle;
21082
21927
  }
21083
- let $win_parent = window_layer_of(host, shell);
21928
+ let $win_parent = window_layer_of(host);
21084
21929
  handle.win = gobj_create_service(`gclass-win-${clean_name(gobj_name(host))}`, "C_YUI_WINDOW", {
21085
21930
  $parent: $win_parent,
21086
21931
  subscriber: null,
@@ -21148,14 +21993,19 @@ function close_gclass_view(handle) {
21148
21993
  * window on `body` and mounted in the shell's popup layer
21149
21994
  * lands BEHIND its opener whatever number it is given, and
21150
21995
  * the button reads as one that did nothing.
21996
+ *
21997
+ * Opened from no window: null -- the window layer of the
21998
+ * shell, where every window lives (the legacy '#top-layer'
21999
+ * when a page has one). It was the shell's popup layer: above
22000
+ * the drawer, in the way of the toolbar's dropdowns.
21151
22001
  ************************************************************/
21152
- function window_layer_of(host, shell) {
22002
+ function window_layer_of(host) {
21153
22003
  let $c = gobj_read_pointer_attr(host, "$container");
21154
22004
  if ($c && typeof $c.closest === "function") {
21155
22005
  let $win = $c.closest(".C_YUI_WINDOW");
21156
22006
  if ($win && $win.parentNode) return $win.parentNode;
21157
22007
  }
21158
- return typeof document !== "undefined" && document.getElementById("top-layer") || shell && yui_shell_popup_layer(shell) || null;
22008
+ return typeof document !== "undefined" && document.getElementById("top-layer") || null;
21159
22009
  }
21160
22010
  /************************************************************
21161
22011
  * A window is not a sheet: below this width there is no room
@@ -21824,6 +22674,8 @@ function make_find_items(gobj) {
21824
22674
  type: "text",
21825
22675
  placeholder: t("search"),
21826
22676
  "data-i18n-placeholder": "search",
22677
+ title: t("search"),
22678
+ "data-i18n-title": "search",
21827
22679
  "aria-label": t("search"),
21828
22680
  "data-i18n-aria-label": "search"
21829
22681
  },
@@ -23609,7 +24461,7 @@ function card_descriptor(it, show_label, badge_of) {
23609
24461
  "aria-label": label || it.id
23610
24462
  };
23611
24463
  if (label) a_attrs["data-i18n-aria-label"] = label;
23612
- let tip = it.tooltip || it.aria_label;
24464
+ let tip = it.tooltip || it.aria_label || label;
23613
24465
  if (tip) {
23614
24466
  a_attrs.title = tip;
23615
24467
  a_attrs["data-i18n-title"] = tip;
@@ -23618,6 +24470,7 @@ function card_descriptor(it, show_label, badge_of) {
23618
24470
  a_attrs["aria-disabled"] = "true";
23619
24471
  a_attrs["tabindex"] = "-1";
23620
24472
  }
24473
+ if (badge) name_control_with_badge(a_attrs, badge);
23621
24474
  return [
23622
24475
  "a",
23623
24476
  a_attrs,
@@ -24020,6 +24873,7 @@ function render_tabs(gobj, items) {
24020
24873
  a_attrs.title = tip;
24021
24874
  a_attrs["data-i18n-title"] = tip;
24022
24875
  }
24876
+ name_control_with_badge(a_attrs, badge_el);
24023
24877
  let li_class = empty_string(it.class) ? "" : String(it.class);
24024
24878
  lis.push([
24025
24879
  "li",
@@ -24099,11 +24953,14 @@ function render_breadcrumb(gobj, items) {
24099
24953
  href: it.route ? "#" + it.route : "#",
24100
24954
  "data-item-id": it.id,
24101
24955
  "data-route": it.route || "",
24102
- "data-disabled": it.disabled ? "1" : "0"
24956
+ "data-disabled": it.disabled ? "1" : "0",
24957
+ "aria-label": it.name || it.id
24103
24958
  };
24104
- if (it.name) {
24105
- a_attrs["aria-label"] = it.name;
24106
- a_attrs["data-i18n-aria-label"] = it.name;
24959
+ if (it.name) a_attrs["data-i18n-aria-label"] = it.name;
24960
+ let tip = it.tooltip || it.aria_label || it.name;
24961
+ if (tip) {
24962
+ a_attrs.title = tip;
24963
+ a_attrs["data-i18n-title"] = tip;
24107
24964
  }
24108
24965
  lis.push([
24109
24966
  "li",
@@ -24236,17 +25093,19 @@ function render_accordion(gobj, items) {
24236
25093
  "aria-expanded": "false",
24237
25094
  "aria-controls": body_id
24238
25095
  };
25096
+ let head_badge = item_badge_node(gobj, it, false);
24239
25097
  if (!empty_string(head_text)) {
24240
25098
  head_attrs.i18n = head_text;
24241
25099
  head_attrs.title = head_text;
24242
25100
  head_attrs["data-i18n-title"] = head_text;
24243
25101
  head_attrs["aria-label"] = head_text;
24244
25102
  head_attrs["data-i18n-aria-label"] = head_text;
25103
+ name_control_with_badge(head_attrs, head_badge);
24245
25104
  }
24246
25105
  let $hdr = createElement2([
24247
25106
  "button",
24248
25107
  head_attrs,
24249
- [text_node(head_text), item_badge_node(gobj, it, false)]
25108
+ [text_node(head_text), head_badge]
24250
25109
  ]);
24251
25110
  $container.appendChild($hdr);
24252
25111
  let $ul = createElement2(["ul", {
@@ -24335,6 +25194,7 @@ function item_li(gobj, it, opts) {
24335
25194
  a_attrs["aria-disabled"] = "true";
24336
25195
  a_attrs["tabindex"] = "-1";
24337
25196
  }
25197
+ name_control_with_badge(a_attrs, badge_el);
24338
25198
  return [
24339
25199
  "li",
24340
25200
  {},
@@ -24381,6 +25241,7 @@ function item_iconbar(gobj, it, opts) {
24381
25241
  a_attrs.title = tip;
24382
25242
  a_attrs["data-i18n-title"] = tip;
24383
25243
  }
25244
+ name_control_with_badge(a_attrs, badge_el);
24384
25245
  return [
24385
25246
  "div",
24386
25247
  { class: "level-item" },
@@ -24638,7 +25499,10 @@ function render_node(node) {
24638
25499
  if (node.icon && /^yi-[a-z0-9-]+$/.test(node.icon)) row.push([
24639
25500
  "span",
24640
25501
  { class: "icon ROUTEMAP_ICON" },
24641
- [["i", { class: node.icon }]]
25502
+ [["i", {
25503
+ class: node.icon,
25504
+ "aria-hidden": "true"
25505
+ }]]
24642
25506
  ]);
24643
25507
  row.push([
24644
25508
  "span",
@@ -24687,16 +25551,23 @@ function render_node(node) {
24687
25551
  ]);
24688
25552
  let row_class = (node.current ? " ROUTEMAP_CURRENT" : "") + (node.ref ? " ROUTEMAP_IS_REF" : "");
24689
25553
  let $row;
24690
- if (node.route) $row = createElement2([
24691
- "a",
24692
- {
25554
+ if (node.route) {
25555
+ let a_attrs = {
24693
25556
  class: "ROUTEMAP_LINK ROUTEMAP_ROW" + row_class,
24694
25557
  href: "#" + node.route,
24695
25558
  title: node.route
24696
- },
24697
- row
24698
- ]);
24699
- else $row = createElement2([
25559
+ };
25560
+ if (node.label) {
25561
+ a_attrs["aria-label"] = node.label;
25562
+ a_attrs["data-i18n-aria-label"] = node.label;
25563
+ }
25564
+ if (node.current) a_attrs["aria-current"] = "page";
25565
+ $row = createElement2([
25566
+ "a",
25567
+ a_attrs,
25568
+ row
25569
+ ]);
25570
+ } else $row = createElement2([
24700
25571
  "span",
24701
25572
  { class: "ROUTEMAP_ROW ROUTEMAP_STRUCT" + row_class },
24702
25573
  row
@@ -24980,7 +25851,7 @@ function yui_shell_show_route_map(shell, opts) {
24980
25851
  if (gclass_find_by_name("C_YUI_WINDOW")) {
24981
25852
  let win_ref = { gobj: null };
24982
25853
  let $body = build_body(shell, t);
24983
- let $parent = yui_shell_popup_layer(shell) || typeof document !== "undefined" && document.getElementById("top-layer") || null;
25854
+ let $parent = typeof document !== "undefined" && document.getElementById("top-layer") || null;
24984
25855
  let win = gobj_create_service(WIN_NAME$2, "C_YUI_WINDOW", {
24985
25856
  $parent,
24986
25857
  subscriber: null,
@@ -26054,7 +26925,13 @@ function add_tab(gobj, gobj2, id, text, icon) {
26054
26925
  let $item = createElement2([
26055
26926
  "li",
26056
26927
  { class: "" },
26057
- [["a", { href: id }]]
26928
+ [["a", {
26929
+ href: id,
26930
+ title: t(text),
26931
+ "data-i18n-title": text,
26932
+ "aria-label": t(text),
26933
+ "data-i18n-aria-label": text
26934
+ }]]
26058
26935
  ]);
26059
26936
  $tabs.appendChild($item);
26060
26937
  $item.gobj = gobj2;
@@ -26062,7 +26939,10 @@ function add_tab(gobj, gobj2, id, text, icon) {
26062
26939
  if (icon) $a.appendChild(createElement2([
26063
26940
  "span",
26064
26941
  { class: "icon is-small is-hidden-mobile" },
26065
- ["i", { class: `${icon}` }]
26942
+ ["i", {
26943
+ class: `${icon}`,
26944
+ "aria-hidden": "true"
26945
+ }]
26066
26946
  ]));
26067
26947
  $a.appendChild(createElement2([
26068
26948
  "span",
@@ -26251,7 +27131,9 @@ function add_topic_card(gobj, id, text, icon, desc) {
26251
27131
  class: "yui-nav-item yui-nav-card TREEDB_TOPIC_CARD",
26252
27132
  href: "#",
26253
27133
  "data-topic-href": id,
26254
- "aria-label": text,
27134
+ title: t(text),
27135
+ "data-i18n-title": text,
27136
+ "aria-label": t(text),
26255
27137
  "data-i18n-aria-label": text
26256
27138
  },
26257
27139
  [[
@@ -27095,7 +27977,7 @@ function open_json_viewer$1(gobj, opts) {
27095
27977
  }
27096
27978
  });
27097
27979
  } else {
27098
- let $win_parent = shell && yui_shell_popup_layer(shell) || typeof document !== "undefined" && document.getElementById("top-layer") || null;
27980
+ let $win_parent = typeof document !== "undefined" && document.getElementById("top-layer") || null;
27099
27981
  priv.json_win = gobj_create_service(`treedb-topics-jsonwin-${clean_name(gobj_name(gobj))}`, "C_YUI_WINDOW", {
27100
27982
  $parent: $win_parent,
27101
27983
  subscriber: null,
@@ -27222,7 +28104,9 @@ function request_print_tranger$1(gobj, path) {
27222
28104
  }, gobj);
27223
28105
  if (ret) {
27224
28106
  log_error(ret);
27225
- answer_print_tranger_refused$1(gobj, path, { error: refusal_text$1(ret) || "print-tranger failed" });
28107
+ const refusal = refusal_text$1(ret);
28108
+ if (refusal) answer_print_tranger_refused$1(gobj, path, { error: refusal });
28109
+ else answer_print_tranger_refused$1(gobj, path, { i18n: "print-tranger failed" });
27226
28110
  return;
27227
28111
  }
27228
28112
  if (path) priv.drills_in_flight.add(path);
@@ -27290,9 +28174,13 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
27290
28174
  }
27291
28175
  } else gobj.priv.drills_in_flight.clear();
27292
28176
  if (result < 0) {
27293
- if (path) gobj_send_event(jv, "EV_SUBTREE_ERROR", {
28177
+ if (path && comment) gobj_send_event(jv, "EV_SUBTREE_ERROR", {
27294
28178
  path,
27295
- error: comment || "print-tranger failed"
28179
+ error: comment
28180
+ }, gobj);
28181
+ else if (path) gobj_send_event(jv, "EV_SUBTREE_ERROR", {
28182
+ path,
28183
+ i18n: "print-tranger failed"
27296
28184
  }, gobj);
27297
28185
  else yui_shell_show_error(yui_shell_of(gobj), comment || "print-tranger failed", { t });
27298
28186
  return 0;
@@ -29077,7 +29965,7 @@ function present_json_popup(gobj, $box, opts) {
29077
29965
  win: null
29078
29966
  };
29079
29967
  let win = gobj_create_service(opts.name, "C_YUI_WINDOW", {
29080
- $parent: popup_mount_layer(gobj),
29968
+ $parent: popup_mount_layer(),
29081
29969
  subscriber: null,
29082
29970
  modal: false,
29083
29971
  showMax: true,
@@ -29131,10 +30019,17 @@ function retire_popup_window(win) {
29131
30019
  log_warning(`${GCLASS_NAME$5}: json window already gone: ${e}`);
29132
30020
  }
29133
30021
  }
29134
- function popup_mount_layer(gobj) {
29135
- let $layer = yui_shell_popup_layer(yui_shell_of(gobj));
29136
- if ($layer) return $layer;
29137
- return document.getElementById("popup-layer") || document.body;
30022
+ /************************************************************
30023
+ * Where a json window of this view lives: null, the WINDOW
30024
+ * layer of the shell, where every window lives -- below the
30025
+ * shell's dialogs, so a confirm asked over it paints above it
30026
+ * (the legacy C_YUI_MAIN '#popup-layer' when a page has one).
30027
+ * It was the shell's popup layer: above the drawer, in the way
30028
+ * of the toolbar's dropdowns, and in an order of its own no
30029
+ * other window could rise above.
30030
+ ************************************************************/
30031
+ function popup_mount_layer() {
30032
+ return document.getElementById("popup-layer") || null;
29138
30033
  }
29139
30034
  /******************************
29140
30035
  * Framework Methods
@@ -29749,7 +30644,10 @@ function col_title_formatter(gobj, col) {
29749
30644
  let text = col_label(col, topic_name);
29750
30645
  let attrs = {};
29751
30646
  const NO_I18N = "\0";
29752
- if (t(col.id, { defaultValue: NO_I18N }) !== NO_I18N) attrs.i18n = col.id;
30647
+ let key = null;
30648
+ if (topic_name && t(topic_name + "." + col.id, { defaultValue: NO_I18N }) !== NO_I18N) key = topic_name + "." + col.id;
30649
+ else if (t(col.id, { defaultValue: NO_I18N }) !== NO_I18N) key = col.id;
30650
+ if (key) attrs.i18n = key;
29753
30651
  return createElement2([
29754
30652
  "span",
29755
30653
  attrs,
@@ -29843,6 +30741,9 @@ function create_tabulator(gobj) {
29843
30741
  colDef.editable = function() {
29844
30742
  return gobj_read_bool_attr(gobj, "editable") && !gobj_read_bool_attr(gobj, "readonly");
29845
30743
  };
30744
+ yui_tabulator_name_editor(colDef, function() {
30745
+ return col_label(col, gobj_read_str_attr(gobj, "topic_name"));
30746
+ });
29846
30747
  }
29847
30748
  const FILTERABLE = [
29848
30749
  "string",
@@ -71201,7 +72102,7 @@ function open_json_viewer(gobj) {
71201
72102
  });
71202
72103
  gobj_start(jv);
71203
72104
  } else {
71204
- let $win_parent = shell && yui_shell_popup_layer(shell) || typeof document !== "undefined" && document.getElementById("top-layer") || null;
72105
+ let $win_parent = typeof document !== "undefined" && document.getElementById("top-layer") || null;
71205
72106
  priv.json_win = gobj_create_service(`treedb-jsonwin-${clean_name(gobj_name(gobj))}`, "C_YUI_WINDOW", {
71206
72107
  $parent: $win_parent,
71207
72108
  subscriber: null,
@@ -71315,7 +72216,9 @@ function request_print_tranger(gobj, path) {
71315
72216
  }, gobj);
71316
72217
  if (ret) {
71317
72218
  log_error(ret);
71318
- answer_print_tranger_refused(gobj, path, { error: refusal_text(ret) || "print-tranger failed" });
72219
+ const refusal = refusal_text(ret);
72220
+ if (refusal) answer_print_tranger_refused(gobj, path, { error: refusal });
72221
+ else answer_print_tranger_refused(gobj, path, { i18n: "print-tranger failed" });
71319
72222
  return;
71320
72223
  }
71321
72224
  if (path) priv.drills_in_flight.add(path);
@@ -71720,9 +72623,13 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
71720
72623
  }
71721
72624
  } else priv.drills_in_flight.clear();
71722
72625
  if (result < 0) {
71723
- if (path) gobj_send_event(jv, "EV_SUBTREE_ERROR", {
72626
+ if (path && comment) gobj_send_event(jv, "EV_SUBTREE_ERROR", {
72627
+ path,
72628
+ error: comment
72629
+ }, gobj);
72630
+ else if (path) gobj_send_event(jv, "EV_SUBTREE_ERROR", {
71724
72631
  path,
71725
- error: comment || "print-tranger failed"
72632
+ i18n: "print-tranger failed"
71726
72633
  }, gobj);
71727
72634
  else yui_shell_show_error(yui_shell_of(gobj), comment || "print-tranger failed", { t });
71728
72635
  return 0;
@@ -83314,7 +84221,8 @@ function apply_dev_traces() {
83314
84221
  * Shell-agnostic: the legacy C_YUI_MAIN shell has a
83315
84222
  * '#top-layer' stacking element; the new C_YUI_SHELL does not.
83316
84223
  * We pass that element when present, otherwise null — C_YUI_WINDOW
83317
- * falls back to document.body by contract.
84224
+ * then takes the window layer of the shell (document.body while
84225
+ * there is none), and follows the shell across a logout.
83318
84226
  ************************************************************/
83319
84227
  function setup_dev(self, show) {
83320
84228
  if (show) {
@@ -83404,7 +84312,8 @@ var WIN_NAME$1 = "Frontend-View-Window";
83404
84312
  * Shell-agnostic: the legacy C_YUI_MAIN shell has a
83405
84313
  * '#top-layer' stacking element; the new C_YUI_SHELL does not.
83406
84314
  * We pass that element when present, otherwise null — C_YUI_WINDOW
83407
- * falls back to document.body by contract.
84315
+ * then takes the window layer of the shell (document.body while
84316
+ * there is none), and follows the shell across a logout.
83408
84317
  *
83409
84318
  * Returns the window gobj, or null.
83410
84319
  ************************************************************/