@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
@@ -50,6 +50,13 @@ import {
50
50
 
51
51
  import {t} from "i18next";
52
52
  import {text_node} from "./yui_text.js";
53
+ import {
54
+ yui_shell_of,
55
+ yui_shell_raise_window,
56
+ yui_shell_watch_changes,
57
+ yui_shell_unwatch_changes,
58
+ } from "./c_yui_shell.js";
59
+ import {yui_shell_popup_layer} from "./shell_modals.js";
53
60
 
54
61
 
55
62
  /***************************************************************
@@ -57,13 +64,18 @@ import {text_node} from "./yui_text.js";
57
64
  ***************************************************************/
58
65
  const GCLASS_NAME = "C_YUI_WINDOW_MANAGER";
59
66
 
60
- /* Dock sits above every window: windows raise to an incrementing
61
- * counter that starts well below this. */
67
+ /* The floating dock sits at the top of the layer it lives in
68
+ * (dock_place()), the shell's popup layer: every window is in the
69
+ * window layer below it, whatever number it takes, and the shell's
70
+ * dialogs and toasts are layers above. */
62
71
  const DOCK_Z = 1000000;
63
72
 
64
73
  /* Chip close glyph (currentColor → theme-aware). */
65
74
  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
75
 
76
+ /* Unique ids for the label spans a chip is named by (aria-labelledby). */
77
+ let __chip_seq__ = 0;
78
+
67
79
 
68
80
  /***************************************************************
69
81
  * Data
@@ -74,14 +86,13 @@ SDATA(data_type_t.DTP_STRING, "dock_mode", 0, "floating", "floatin
74
86
  SDATA(data_type_t.DTP_STRING, "dock_corner", 0, "bottom-left", "Floating placement: top-left | top-right | bottom-left | bottom-right"),
75
87
  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
88
  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)"),
89
+ 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
90
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: the dock element"),
79
91
  SDATA_END()
80
92
  ];
81
93
 
82
94
  let PRIVATE_DATA = {
83
95
  windows: null, /* [{gobj, $chip, minimized}] */
84
- z: 0, /* z-order counter handed to focused windows */
85
96
  mql: null, /* MediaQueryList when dock_mode is 'responsive' */
86
97
  mql_handler: null, /* its change listener (kept for teardown) */
87
98
  };
@@ -106,7 +117,6 @@ function mt_create(gobj)
106
117
  let priv = gobj.priv;
107
118
 
108
119
  priv.windows = [];
109
- priv.z = 10;
110
120
 
111
121
  /* In 'responsive' mode the placement follows a media query: watch it
112
122
  * so the dock re-homes (floating <-> inline) when the viewport crosses
@@ -123,6 +133,13 @@ function mt_create(gobj)
123
133
 
124
134
  build_dock(gobj);
125
135
 
136
+ /* The dock lives in a shell (its popup layer, or a zone when
137
+ * inline), and it outlives the shell: the app builds it once, a
138
+ * logout destroys the shell, the login builds another. Told when
139
+ * a shell is built or destroyed, it places itself again
140
+ * (ac_shell_changed). Unwatched in mt_destroy. */
141
+ yui_shell_watch_changes(gobj);
142
+
126
143
  /*
127
144
  * SERVICE subscription model
128
145
  */
@@ -139,6 +156,8 @@ function mt_destroy(gobj)
139
156
  {
140
157
  let priv = gobj.priv;
141
158
 
159
+ yui_shell_unwatch_changes(gobj);
160
+
142
161
  if(priv.mql && priv.mql_handler) {
143
162
  priv.mql.removeEventListener("change", priv.mql_handler);
144
163
  priv.mql = null;
@@ -200,10 +219,18 @@ function ensure_dock_style()
200
219
  }
201
220
  .yui-dock-chip {
202
221
  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;
222
+ padding: 0 5px 0 0; border: 1px solid var(--bulma-border); border-radius: 7px;
223
+ background: var(--bulma-scheme-main); color: var(--bulma-text); white-space: nowrap;
205
224
  }
206
225
  .yui-dock-chip:hover { border-color: var(--bulma-text-weak); }
226
+ /* The chip is the look; its toggle a button that adds none, and
227
+ * takes the pill's padding, so the whole label is what a press lands
228
+ * on and the chip reads as one pill with its ✕ at the end. */
229
+ .yui-dock-chip-toggle {
230
+ display: inline-flex; align-items: center; gap: 7px;
231
+ padding: 4px 0 4px 11px; margin: 0; border: 0; border-radius: 7px 0 0 7px;
232
+ background: transparent; font: inherit; color: inherit; cursor: pointer; white-space: nowrap;
233
+ }
207
234
  /* The BORDER says active, not the text: blue ink on a blue tint is
208
235
  * the same hue on itself -- 4.49:1 on the light chip, under the 4.5
209
236
  * its 12px label needs. The label takes the text colour (11:1). */
@@ -307,12 +334,37 @@ function apply_placement(gobj)
307
334
  $dock.classList.toggle(`yui-dock--${c}`, !inline && c === corner);
308
335
  }
309
336
 
310
- let target = inline ? $inline : (gobj_read_attr(gobj, "$parent") || document.body);
337
+ let target = inline ? $inline : (gobj_read_attr(gobj, "$parent") || dock_place(gobj));
311
338
  if($dock.parentNode !== target) {
312
339
  target.appendChild($dock);
313
340
  }
314
341
  }
315
342
 
343
+ /************************************************************
344
+ * Where the FLOATING dock lives when the app names no
345
+ * `$parent`: the popup layer of the shell, at the top of it.
346
+ *
347
+ * It was document.body at z-index 1000000, and that put it
348
+ * above the WHOLE shell -- `.yui-shell` is a stacking context
349
+ * of its own, so its dialogs (modal layer, 99) and toasts (120)
350
+ * counted only inside it: the dock stayed clickable over a
351
+ * dialog's backdrop (a window could be closed from its chip
352
+ * while its own "all changes will be lost" question was open),
353
+ * and it covered a toast where they met.
354
+ *
355
+ * The popup layer, not the window layer: the dock has to stay
356
+ * above EVERY window, and windows live in two layers -- the
357
+ * window layer (no `$parent`) and the popup layer, where the
358
+ * site map and the JSON windows of the treedb views put theirs.
359
+ * Above both; below the dialogs, the toasts and the loading
360
+ * cover. No shell (before the first, between a logout and the
361
+ * next login): the body.
362
+ ************************************************************/
363
+ function dock_place(gobj)
364
+ {
365
+ return yui_shell_popup_layer(yui_shell_of(gobj)) || document.body;
366
+ }
367
+
316
368
  /************************************************************
317
369
  * Destroy the dock element.
318
370
  ************************************************************/
@@ -416,7 +468,6 @@ function set_active(gobj, entry)
416
468
  ************************************************************/
417
469
  function focus_entry(gobj, entry)
418
470
  {
419
- let priv = gobj.priv;
420
471
  let $win = gobj_read_attr(entry.gobj, "$container");
421
472
  if($win) {
422
473
  if(entry.minimized) {
@@ -428,8 +479,10 @@ function focus_entry(gobj, entry)
428
479
  entry.$chip.classList.remove('is-min');
429
480
  }
430
481
  }
431
- priv.z += 1;
432
- $win.style.zIndex = priv.z;
482
+ /* The windows' ONE z-order, shared with the windows that have
483
+ * no dock (yui_shell_raise_window). A counter of its own put a
484
+ * window opened later without a dock UNDER every docked one. */
485
+ yui_shell_raise_window($win);
433
486
  }
434
487
  set_active(gobj, entry);
435
488
  }
@@ -525,21 +578,52 @@ function ac_register_window(gobj, event, kw, src)
525
578
  text_node(title)]); // data, not markup
526
579
  }
527
580
 
581
+ /* The toggle's NAME is its label, read from the label's own spans
582
+ * (aria-labelledby): the data half as it is, the kind half from the
583
+ * node that carries its key -- so the name changes language with the
584
+ * label, which a composed aria-label could not. */
585
+ let chip_seq = ++__chip_seq__;
586
+ let label_ids = label_items.map(function(item, i) {
587
+ item[1].id = `yui-dock-chip-${chip_seq}-${i}`;
588
+ return item[1].id;
589
+ });
590
+
528
591
  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. */
592
+ /* The chip is a CONTAINER of two sibling controls, and no control
593
+ * itself: the TOGGLE (minimise / bring back, the whole label) and
594
+ * the ✕ (EV_CLOSE_WINDOW → the window's own teardown →
595
+ * EV_UNREGISTER_WINDOW removes this chip). Up to 7.26.34 the chip
596
+ * was a role=button div HOLDING the ✕ button -- one control inside
597
+ * another, whose name read the label and then "close", and whose
598
+ * ✕ a reader could not reach as a control of its own.
599
+ *
600
+ * Both are real <button>s: a key presses them, with no handler of
601
+ * ours. Each says what it does with a title and an aria-label: the
602
+ * toggle the window's composed title (with its key as
603
+ * data-i18n-title / -aria-label when there is no DATA half; a
604
+ * reader takes the aria-labelledby, which changes language with the
605
+ * label), the ✕ "close". The toggle's click crosses the FSM
606
+ * (EV_TOGGLE_WINDOW), by the window's name: a kw is plain json. */
607
+ let window_name = gobj_full_name(win);
535
608
  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',
609
+ ['div', {class: 'DOCK_CHIP yui-dock-chip'}, [
610
+ ['button', Object.assign(
611
+ {class: 'DOCK_CHIP_TOGGLE yui-dock-chip-toggle', type: 'button',
612
+ title: title, 'aria-label': title, 'aria-labelledby': label_ids.join(" ")},
613
+ (!prefix && title_key) ? {
614
+ 'data-i18n-title': /* i18n: data */ title_key,
615
+ 'data-i18n-aria-label': /* i18n: data */ title_key
616
+ } : {}
617
+ ), [
618
+ chip_lead(kw.icon),
619
+ ['span', {class: 'yui-dock-label'}, label_items]
620
+ ], {
621
+ click: (evt) => {
622
+ evt.stopPropagation();
623
+ gobj_send_event(gobj, "EV_TOGGLE_WINDOW", {window: window_name}, gobj);
624
+ }
625
+ }],
626
+ ['button', {class: 'DOCK_CHIP_CLOSE yui-dock-close', type: 'button',
543
627
  title: t('close'), 'data-i18n-title': 'close',
544
628
  'aria-label': t('close'), 'data-i18n-aria-label': 'close'}, html_markup(WC_X), {
545
629
  click: (evt) => {
@@ -547,26 +631,7 @@ function ac_register_window(gobj, event, kw, src)
547
631
  gobj_send_event(entry.gobj, "EV_CLOSE_WINDOW", {}, gobj);
548
632
  }
549
633
  }],
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
- }]
634
+ ]]
570
635
  );
571
636
  entry.$chip = $chip;
572
637
  priv.windows.push(entry);
@@ -635,6 +700,49 @@ function ac_focus_window(gobj, event, kw, src)
635
700
  return 0;
636
701
  }
637
702
 
703
+ /************************************************************
704
+ * EV_TOGGLE_WINDOW { window } -- the toggle of a chip of THIS
705
+ * dock was pressed: bring a minimized or covered window to the
706
+ * front, send the one in front to the dock. Only the dock's own
707
+ * chip sends it (src is the dock): the events a window sends
708
+ * about itself come from the window, and this one from the chip
709
+ * that names it.
710
+ ************************************************************/
711
+ function ac_toggle_window(gobj, event, kw, src)
712
+ {
713
+ let priv = gobj.priv;
714
+
715
+ if(src !== gobj) {
716
+ log_error(
717
+ `${gobj_short_name(gobj)}: ${event} comes from the dock's own chip, ` +
718
+ `not from ${is_gobj(src) ? gobj_full_name(src) : String(src)}`
719
+ );
720
+ return -1;
721
+ }
722
+ let name = kw && kw.window;
723
+ let entry = priv.windows.find((e) => gobj_full_name(e.gobj) === name);
724
+ if(!entry) {
725
+ log_error(`${gobj_short_name(gobj)}: ${event}: no window '${name}' in the dock`);
726
+ return -1;
727
+ }
728
+ on_chip_click(gobj, entry);
729
+ return 0;
730
+ }
731
+
732
+ /************************************************************
733
+ * EV_SHELL_CHANGED -- a shell was built, or is being destroyed
734
+ * (yui_shell_watch_changes()). The dock's place -- the shell's
735
+ * popup layer, or its zone when inline -- may have gone with
736
+ * it, or be in the new one: place it again. Up to 7.26.34 an
737
+ * inline dock stayed in the destroyed shell's zone after a
738
+ * logout, detached, until a window registered.
739
+ ************************************************************/
740
+ function ac_shell_changed(gobj, event, kw, src)
741
+ {
742
+ apply_placement(gobj);
743
+ return 0;
744
+ }
745
+
638
746
 
639
747
 
640
748
 
@@ -672,6 +780,8 @@ function create_gclass(gclass_name)
672
780
  ["EV_UNREGISTER_WINDOW", ac_unregister_window, null],
673
781
  ["EV_MINIMIZE_WINDOW", ac_minimize_window, null],
674
782
  ["EV_FOCUS_WINDOW", ac_focus_window, null],
783
+ ["EV_TOGGLE_WINDOW", ac_toggle_window, null],
784
+ ["EV_SHELL_CHANGED", ac_shell_changed, null],
675
785
  ]]
676
786
  ];
677
787
 
@@ -683,6 +793,8 @@ function create_gclass(gclass_name)
683
793
  ["EV_UNREGISTER_WINDOW", 0],
684
794
  ["EV_MINIMIZE_WINDOW", 0],
685
795
  ["EV_FOCUS_WINDOW", 0],
796
+ ["EV_TOGGLE_WINDOW", 0],
797
+ ["EV_SHELL_CHANGED", 0],
686
798
  ];
687
799
 
688
800
  __gclass__ = gclass_create(