@yuneta/gobj-ui 7.26.34 → 7.26.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +223 -53
  2. package/dist/gobj-ui.cjs.js +1109 -143
  3. package/dist/gobj-ui.es.js +1109 -143
  4. package/package.json +2 -2
  5. package/scripts/validate-locales.mjs +672 -96
  6. package/src/c_yui_form.js +186 -9
  7. package/src/c_yui_gclass.js +2 -0
  8. package/src/c_yui_gobj_tree_js.js +2 -0
  9. package/src/c_yui_json.js +2 -0
  10. package/src/c_yui_json_graph.js +2 -0
  11. package/src/c_yui_nav.js +23 -5
  12. package/src/c_yui_shell.css +16 -0
  13. package/src/c_yui_shell.js +282 -26
  14. package/src/c_yui_treedb_graph.js +26 -8
  15. package/src/c_yui_treedb_topic_with_form.js +40 -18
  16. package/src/c_yui_treedb_topics.js +34 -11
  17. package/src/c_yui_window.js +132 -32
  18. package/src/c_yui_window_manager.js +250 -46
  19. package/src/form_json_editor_names.js +448 -0
  20. package/src/form_json_editor_names.wiring.test.js +595 -0
  21. package/src/form_label_i18n.wiring.test.js +169 -0
  22. package/src/i18n_key_functions.js +14 -6
  23. package/src/i18n_key_functions.test.js +124 -2
  24. package/src/lib_maplibre.js +26 -2
  25. package/src/lib_maplibre_relocalize.test.js +78 -0
  26. package/src/named_controls.wiring.test.js +468 -0
  27. package/src/nav_cards_helpers.js +9 -1
  28. package/src/nav_item_badge.wiring.test.js +182 -1
  29. package/src/shell_route_map.js +25 -8
  30. package/src/table_editor_name.wiring.test.js +201 -0
  31. package/src/treedb_column_title.wiring.test.js +151 -0
  32. package/src/treedb_json_drill.wiring.test.js +41 -1
  33. package/src/window_layer.wiring.test.js +348 -43
  34. package/src/window_show.wiring.test.js +6 -2
  35. package/src/window_stacking.wiring.test.js +334 -0
  36. package/src/yui_badge.js +58 -13
  37. package/src/yui_badge.test.js +40 -3
  38. package/src/yui_dev.js +2 -1
  39. package/src/yui_file_field.js +2 -0
  40. package/src/yui_frontend_view.js +2 -1
  41. package/src/yui_gclass_view.js +9 -6
  42. package/src/yui_tabulator_i18n.js +42 -0
  43. package/src/yui_tabulator_i18n.test.js +48 -1
@@ -40,6 +40,8 @@ import {
40
40
  gobj_short_name,
41
41
  gobj_read_pointer_attr,
42
42
  gobj_subscribe_event,
43
+ gobj_unsubscribe_event,
44
+ gobj_is_destroying,
43
45
  gobj_send_event,
44
46
  createElement2,
45
47
  gobj_write_attr,
@@ -50,6 +52,13 @@ import {
50
52
 
51
53
  import {t} from "i18next";
52
54
  import {text_node} from "./yui_text.js";
55
+ import {
56
+ yui_shell_of,
57
+ yui_shell_raise_window,
58
+ yui_shell_watch_changes,
59
+ yui_shell_unwatch_changes,
60
+ } from "./c_yui_shell.js";
61
+ import {yui_shell_popup_layer} from "./shell_modals.js";
53
62
 
54
63
 
55
64
  /***************************************************************
@@ -57,13 +66,18 @@ import {text_node} from "./yui_text.js";
57
66
  ***************************************************************/
58
67
  const GCLASS_NAME = "C_YUI_WINDOW_MANAGER";
59
68
 
60
- /* Dock sits above every window: windows raise to an incrementing
61
- * counter that starts well below this. */
69
+ /* The floating dock sits at the top of the layer it lives in
70
+ * (dock_place()), the shell's popup layer: every window is in the
71
+ * window layer below it, whatever number it takes, and the shell's
72
+ * dialogs and toasts are layers above. */
62
73
  const DOCK_Z = 1000000;
63
74
 
64
75
  /* Chip close glyph (currentColor → theme-aware). */
65
76
  const 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>';
66
77
 
78
+ /* Unique ids for the label spans a chip is named by (aria-labelledby). */
79
+ let __chip_seq__ = 0;
80
+
67
81
 
68
82
  /***************************************************************
69
83
  * Data
@@ -74,16 +88,16 @@ SDATA(data_type_t.DTP_STRING, "dock_mode", 0, "floating", "floatin
74
88
  SDATA(data_type_t.DTP_STRING, "dock_corner", 0, "bottom-left", "Floating placement: top-left | top-right | bottom-left | bottom-right"),
75
89
  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)"),
76
90
  SDATA(data_type_t.DTP_STRING, "responsive_query", 0, "(max-width: 768px)", "Media query that selects inline placement when dock_mode is 'responsive'"),
77
- SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "Where the floating dock mounts (default document.body)"),
91
+ 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)"),
78
92
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: the dock element"),
79
93
  SDATA_END()
80
94
  ];
81
95
 
82
96
  let PRIVATE_DATA = {
83
- windows: null, /* [{gobj, $chip, minimized}] */
84
- z: 0, /* z-order counter handed to focused windows */
97
+ windows: null, /* [{gobj, $chip, minimized, title, title_key, title_prefix}] */
85
98
  mql: null, /* MediaQueryList when dock_mode is 'responsive' */
86
99
  mql_handler: null, /* its change listener (kept for teardown) */
100
+ lang_shell: null, /* the shell whose EV_LANGUAGE_CHANGED we hear */
87
101
  };
88
102
 
89
103
  let __gclass__ = null;
@@ -106,7 +120,6 @@ function mt_create(gobj)
106
120
  let priv = gobj.priv;
107
121
 
108
122
  priv.windows = [];
109
- priv.z = 10;
110
123
 
111
124
  /* In 'responsive' mode the placement follows a media query: watch it
112
125
  * so the dock re-homes (floating <-> inline) when the viewport crosses
@@ -123,6 +136,14 @@ function mt_create(gobj)
123
136
 
124
137
  build_dock(gobj);
125
138
 
139
+ /* The dock lives in a shell (its popup layer, or a zone when
140
+ * inline), and it outlives the shell: the app builds it once, a
141
+ * logout destroys the shell, the login builds another. Told when
142
+ * a shell is built or destroyed, it places itself again
143
+ * (ac_shell_changed). Unwatched in mt_destroy. */
144
+ yui_shell_watch_changes(gobj);
145
+ follow_language(gobj);
146
+
126
147
  /*
127
148
  * SERVICE subscription model
128
149
  */
@@ -139,6 +160,14 @@ function mt_destroy(gobj)
139
160
  {
140
161
  let priv = gobj.priv;
141
162
 
163
+ yui_shell_unwatch_changes(gobj);
164
+ if(priv.lang_shell) {
165
+ if(!gobj_is_destroying(priv.lang_shell)) {
166
+ gobj_unsubscribe_event(priv.lang_shell, "EV_LANGUAGE_CHANGED", {}, gobj);
167
+ }
168
+ priv.lang_shell = null;
169
+ }
170
+
142
171
  if(priv.mql && priv.mql_handler) {
143
172
  priv.mql.removeEventListener("change", priv.mql_handler);
144
173
  priv.mql = null;
@@ -158,6 +187,54 @@ function mt_destroy(gobj)
158
187
 
159
188
 
160
189
 
190
+ /************************************************************
191
+ * Hear the language of the shell that holds the dock -- the
192
+ * dock outlives a shell, so it follows them: the dying one's
193
+ * subscription goes with it, the next one is heard. The
194
+ * toggle of a chip whose window's title has a DATA half has a
195
+ * composed title, which no key carries: it is written again in
196
+ * the new language (ac_language_changed).
197
+ ************************************************************/
198
+ function follow_language(gobj)
199
+ {
200
+ let priv = gobj.priv;
201
+
202
+ let shell = yui_shell_of(gobj);
203
+ if(shell && gobj_is_destroying(shell)) {
204
+ shell = null;
205
+ }
206
+ if(shell === priv.lang_shell) {
207
+ return;
208
+ }
209
+ if(priv.lang_shell && !gobj_is_destroying(priv.lang_shell)) {
210
+ gobj_unsubscribe_event(priv.lang_shell, "EV_LANGUAGE_CHANGED", {}, gobj);
211
+ }
212
+ priv.lang_shell = null;
213
+ if(shell) {
214
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
215
+ priv.lang_shell = shell;
216
+ }
217
+ }
218
+
219
+ /************************************************************
220
+ * The toggle's title and aria-label, in the current language:
221
+ * the same join as C_YUI_WINDOW's composed_title(), DATA half
222
+ * first. The title the window registered with when it gave no
223
+ * halves (a legacy caller).
224
+ ************************************************************/
225
+ function chip_title(entry)
226
+ {
227
+ /* `title_key` is the window's title, a key its HOST chose. */
228
+ let kind = entry.title_key ? t(/* i18n: data */ entry.title_key) : "";
229
+ if(entry.title_prefix && kind) {
230
+ return `${entry.title_prefix} · ${kind}`;
231
+ }
232
+ return entry.title_prefix || kind || entry.title;
233
+ }
234
+
235
+
236
+
237
+
161
238
  /************************************************************
162
239
  * Inject the dock stylesheet once (theme-aware).
163
240
  ************************************************************/
@@ -200,10 +277,18 @@ function ensure_dock_style()
200
277
  }
201
278
  .yui-dock-chip {
202
279
  display: inline-flex; align-items: center; gap: 7px; font: inherit; font-size: 12px;
203
- padding: 4px 5px 4px 11px; border: 1px solid var(--bulma-border); border-radius: 7px;
204
- background: var(--bulma-scheme-main); color: var(--bulma-text); cursor: pointer; white-space: nowrap;
280
+ padding: 0 5px 0 0; border: 1px solid var(--bulma-border); border-radius: 7px;
281
+ background: var(--bulma-scheme-main); color: var(--bulma-text); white-space: nowrap;
205
282
  }
206
283
  .yui-dock-chip:hover { border-color: var(--bulma-text-weak); }
284
+ /* The chip is the look; its toggle a button that adds none, and
285
+ * takes the pill's padding, so the whole label is what a press lands
286
+ * on and the chip reads as one pill with its ✕ at the end. */
287
+ .yui-dock-chip-toggle {
288
+ display: inline-flex; align-items: center; gap: 7px;
289
+ padding: 4px 0 4px 11px; margin: 0; border: 0; border-radius: 7px 0 0 7px;
290
+ background: transparent; font: inherit; color: inherit; cursor: pointer; white-space: nowrap;
291
+ }
207
292
  /* The BORDER says active, not the text: blue ink on a blue tint is
208
293
  * the same hue on itself -- 4.49:1 on the light chip, under the 4.5
209
294
  * its 12px label needs. The label takes the text colour (11:1). */
@@ -307,12 +392,37 @@ function apply_placement(gobj)
307
392
  $dock.classList.toggle(`yui-dock--${c}`, !inline && c === corner);
308
393
  }
309
394
 
310
- let target = inline ? $inline : (gobj_read_attr(gobj, "$parent") || document.body);
395
+ let target = inline ? $inline : (gobj_read_attr(gobj, "$parent") || dock_place(gobj));
311
396
  if($dock.parentNode !== target) {
312
397
  target.appendChild($dock);
313
398
  }
314
399
  }
315
400
 
401
+ /************************************************************
402
+ * Where the FLOATING dock lives when the app names no
403
+ * `$parent`: the popup layer of the shell, at the top of it.
404
+ *
405
+ * It was document.body at z-index 1000000, and that put it
406
+ * above the WHOLE shell -- `.yui-shell` is a stacking context
407
+ * of its own, so its dialogs (modal layer, 99) and toasts (120)
408
+ * counted only inside it: the dock stayed clickable over a
409
+ * dialog's backdrop (a window could be closed from its chip
410
+ * while its own "all changes will be lost" question was open),
411
+ * and it covered a toast where they met.
412
+ *
413
+ * The popup layer, not the window layer: the dock has to stay
414
+ * above EVERY window, and windows live in two layers -- the
415
+ * window layer (no `$parent`) and the popup layer, where the
416
+ * site map and the JSON windows of the treedb views put theirs.
417
+ * Above both; below the dialogs, the toasts and the loading
418
+ * cover. No shell (before the first, between a logout and the
419
+ * next login): the body.
420
+ ************************************************************/
421
+ function dock_place(gobj)
422
+ {
423
+ return yui_shell_popup_layer(yui_shell_of(gobj)) || document.body;
424
+ }
425
+
316
426
  /************************************************************
317
427
  * Destroy the dock element.
318
428
  ************************************************************/
@@ -416,7 +526,6 @@ function set_active(gobj, entry)
416
526
  ************************************************************/
417
527
  function focus_entry(gobj, entry)
418
528
  {
419
- let priv = gobj.priv;
420
529
  let $win = gobj_read_attr(entry.gobj, "$container");
421
530
  if($win) {
422
531
  if(entry.minimized) {
@@ -428,8 +537,10 @@ function focus_entry(gobj, entry)
428
537
  entry.$chip.classList.remove('is-min');
429
538
  }
430
539
  }
431
- priv.z += 1;
432
- $win.style.zIndex = priv.z;
540
+ /* The windows' ONE z-order, shared with the windows that have
541
+ * no dock (yui_shell_raise_window). A counter of its own put a
542
+ * window opened later without a dock UNDER every docked one. */
543
+ yui_shell_raise_window($win);
433
544
  }
434
545
  set_active(gobj, entry);
435
546
  }
@@ -525,21 +636,53 @@ function ac_register_window(gobj, event, kw, src)
525
636
  text_node(title)]); // data, not markup
526
637
  }
527
638
 
528
- let entry = {gobj: win, $chip: null, minimized: false};
529
- /* A div (not a button) so it can hold the close button — a
530
- * button inside a button is invalid. The chip toggles; the ✕
531
- * closes the window (EV_CLOSE_WINDOW → the window's own teardown
532
- * → EV_UNREGISTER_WINDOW removes this chip).
533
- * Tooltip: the composed string; when there is no DATA half it
534
- * carries the key as data-i18n-title so it re-translates too. */
639
+ /* The toggle's NAME is its label, read from the label's own spans
640
+ * (aria-labelledby): the data half as it is, the kind half from the
641
+ * node that carries its key -- so the name changes language with the
642
+ * label, which a composed aria-label could not. */
643
+ let chip_seq = ++__chip_seq__;
644
+ let label_ids = label_items.map(function(item, i) {
645
+ item[1].id = `yui-dock-chip-${chip_seq}-${i}`;
646
+ return item[1].id;
647
+ });
648
+
649
+ let entry = {gobj: win, $chip: null, minimized: false,
650
+ title: title, title_key: title_key, title_prefix: prefix};
651
+ /* The chip is a CONTAINER of two sibling controls, and no control
652
+ * itself: the TOGGLE (minimise / bring back, the whole label) and
653
+ * the ✕ (EV_CLOSE_WINDOW → the window's own teardown →
654
+ * EV_UNREGISTER_WINDOW removes this chip). Up to 7.26.34 the chip
655
+ * was a role=button div HOLDING the ✕ button -- one control inside
656
+ * another, whose name read the label and then "close", and whose
657
+ * ✕ a reader could not reach as a control of its own.
658
+ *
659
+ * Both are real <button>s: a key presses them, with no handler of
660
+ * ours. Each says what it does with a title and an aria-label: the
661
+ * toggle the window's composed title (with its key as
662
+ * data-i18n-title / -aria-label when there is no DATA half; a
663
+ * reader takes the aria-labelledby, which changes language with the
664
+ * label), the ✕ "close". The toggle's click crosses the FSM
665
+ * (EV_TOGGLE_WINDOW), by the window's name: a kw is plain json. */
666
+ let window_name = gobj_full_name(win);
535
667
  let $chip = createElement2(
536
- ['div', Object.assign(
537
- {class: 'yui-dock-chip', role: 'button', tabindex: '0', title: title},
538
- (!prefix && title_key) ? {'data-i18n-title': /* i18n: data */ title_key} : {}
539
- ), [
540
- chip_lead(kw.icon),
541
- ['span', {class: 'yui-dock-label'}, label_items],
542
- ['button', {class: 'yui-dock-close', type: 'button',
668
+ ['div', {class: 'DOCK_CHIP yui-dock-chip'}, [
669
+ ['button', Object.assign(
670
+ {class: 'DOCK_CHIP_TOGGLE yui-dock-chip-toggle', type: 'button',
671
+ title: title, 'aria-label': title, 'aria-labelledby': label_ids.join(" ")},
672
+ (!prefix && title_key) ? {
673
+ 'data-i18n-title': /* i18n: data */ title_key,
674
+ 'data-i18n-aria-label': /* i18n: data */ title_key
675
+ } : {}
676
+ ), [
677
+ chip_lead(kw.icon),
678
+ ['span', {class: 'yui-dock-label'}, label_items]
679
+ ], {
680
+ click: (evt) => {
681
+ evt.stopPropagation();
682
+ gobj_send_event(gobj, "EV_TOGGLE_WINDOW", {window: window_name}, gobj);
683
+ }
684
+ }],
685
+ ['button', {class: 'DOCK_CHIP_CLOSE yui-dock-close', type: 'button',
543
686
  title: t('close'), 'data-i18n-title': 'close',
544
687
  'aria-label': t('close'), 'data-i18n-aria-label': 'close'}, html_markup(WC_X), {
545
688
  click: (evt) => {
@@ -547,26 +690,7 @@ function ac_register_window(gobj, event, kw, src)
547
690
  gobj_send_event(entry.gobj, "EV_CLOSE_WINDOW", {}, gobj);
548
691
  }
549
692
  }],
550
- ], {
551
- click: (evt) => {
552
- evt.stopPropagation();
553
- on_chip_click(gobj, entry);
554
- },
555
- keydown: (evt) => {
556
- /* Only the chip itself — Enter on the inner ✕ button
557
- * already fires its own click. */
558
- if(evt.target !== evt.currentTarget) {
559
- return;
560
- }
561
- if(evt.key === "Enter" || evt.key === " ") {
562
- if(evt.key === " ") {
563
- evt.preventDefault();
564
- }
565
- evt.stopPropagation();
566
- on_chip_click(gobj, entry);
567
- }
568
- }
569
- }]
693
+ ]]
570
694
  );
571
695
  entry.$chip = $chip;
572
696
  priv.windows.push(entry);
@@ -635,6 +759,80 @@ function ac_focus_window(gobj, event, kw, src)
635
759
  return 0;
636
760
  }
637
761
 
762
+ /************************************************************
763
+ * EV_TOGGLE_WINDOW { window } -- the toggle of a chip of THIS
764
+ * dock was pressed: bring a minimized or covered window to the
765
+ * front, send the one in front to the dock. Only the dock's own
766
+ * chip sends it (src is the dock): the events a window sends
767
+ * about itself come from the window, and this one from the chip
768
+ * that names it.
769
+ ************************************************************/
770
+ function ac_toggle_window(gobj, event, kw, src)
771
+ {
772
+ let priv = gobj.priv;
773
+
774
+ if(src !== gobj) {
775
+ log_error(
776
+ `${gobj_short_name(gobj)}: ${event} comes from the dock's own chip, ` +
777
+ `not from ${is_gobj(src) ? gobj_full_name(src) : String(src)}`
778
+ );
779
+ return -1;
780
+ }
781
+ let name = kw && kw.window;
782
+ let entry = priv.windows.find((e) => gobj_full_name(e.gobj) === name);
783
+ if(!entry) {
784
+ log_error(`${gobj_short_name(gobj)}: ${event}: no window '${name}' in the dock`);
785
+ return -1;
786
+ }
787
+ on_chip_click(gobj, entry);
788
+ return 0;
789
+ }
790
+
791
+ /************************************************************
792
+ * EV_SHELL_CHANGED -- a shell was built, or is being destroyed
793
+ * (yui_shell_watch_changes()). The dock's place -- the shell's
794
+ * popup layer, or its zone when inline -- may have gone with
795
+ * it, or be in the new one: place it again. Up to 7.26.34 an
796
+ * inline dock stayed in the destroyed shell's zone after a
797
+ * logout, detached, until a window registered. Its language
798
+ * is the new shell's too.
799
+ ************************************************************/
800
+ function ac_shell_changed(gobj, event, kw, src)
801
+ {
802
+ apply_placement(gobj);
803
+ follow_language(gobj);
804
+ return 0;
805
+ }
806
+
807
+ /************************************************************
808
+ * EV_LANGUAGE_CHANGED -- from the shell that holds the dock.
809
+ * A chip's label re-translates by its key (refresh_language()
810
+ * reaches its kind half); the toggle's title and aria-label
811
+ * of a window with a DATA half are COMPOSED, so no key carries
812
+ * them: written again here. Up to 7.26.35 they stayed in the
813
+ * language the window was opened in (a reader took the
814
+ * aria-labelledby, which changed; the tooltip did not).
815
+ ************************************************************/
816
+ function ac_language_changed(gobj, event, kw, src)
817
+ {
818
+ let priv = gobj.priv;
819
+
820
+ for(let entry of priv.windows) {
821
+ if(!entry.title_prefix || !entry.$chip) {
822
+ continue; // a key alone: data-i18n-title / -aria-label re-translate it
823
+ }
824
+ let $toggle = entry.$chip.querySelector(".DOCK_CHIP_TOGGLE");
825
+ if(!$toggle) {
826
+ log_error(`${gobj_short_name(gobj)}: a chip without its toggle`);
827
+ continue;
828
+ }
829
+ let title = chip_title(entry);
830
+ $toggle.setAttribute("title", title);
831
+ $toggle.setAttribute("aria-label", title);
832
+ }
833
+ return 0;
834
+ }
835
+
638
836
 
639
837
 
640
838
 
@@ -672,6 +870,9 @@ function create_gclass(gclass_name)
672
870
  ["EV_UNREGISTER_WINDOW", ac_unregister_window, null],
673
871
  ["EV_MINIMIZE_WINDOW", ac_minimize_window, null],
674
872
  ["EV_FOCUS_WINDOW", ac_focus_window, null],
873
+ ["EV_TOGGLE_WINDOW", ac_toggle_window, null],
874
+ ["EV_SHELL_CHANGED", ac_shell_changed, null],
875
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
675
876
  ]]
676
877
  ];
677
878
 
@@ -683,6 +884,9 @@ function create_gclass(gclass_name)
683
884
  ["EV_UNREGISTER_WINDOW", 0],
684
885
  ["EV_MINIMIZE_WINDOW", 0],
685
886
  ["EV_FOCUS_WINDOW", 0],
887
+ ["EV_TOGGLE_WINDOW", 0],
888
+ ["EV_SHELL_CHANGED", 0],
889
+ ["EV_LANGUAGE_CHANGED", 0],
686
890
  ];
687
891
 
688
892
  __gclass__ = gclass_create(