@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.
- package/README.md +223 -53
- package/dist/gobj-ui.cjs.js +1053 -144
- package/dist/gobj-ui.es.js +1053 -144
- package/package.json +2 -2
- package/scripts/validate-locales.mjs +672 -96
- package/src/c_yui_form.js +229 -11
- package/src/c_yui_gclass.js +2 -0
- package/src/c_yui_gobj_tree_js.js +2 -0
- package/src/c_yui_json.js +2 -0
- package/src/c_yui_json_graph.js +2 -0
- package/src/c_yui_nav.js +23 -5
- package/src/c_yui_shell.css +16 -0
- package/src/c_yui_shell.js +282 -26
- package/src/c_yui_treedb_graph.js +26 -8
- package/src/c_yui_treedb_topic_with_form.js +40 -18
- package/src/c_yui_treedb_topics.js +34 -11
- package/src/c_yui_window.js +132 -32
- package/src/c_yui_window_manager.js +156 -44
- package/src/form_json_editor_names.js +448 -0
- package/src/form_json_editor_names.wiring.test.js +595 -0
- package/src/form_label_i18n.wiring.test.js +169 -0
- package/src/form_table_validator.wiring.test.js +192 -0
- package/src/form_template_field.wiring.test.js +21 -0
- package/src/i18n_key_functions.js +14 -6
- package/src/i18n_key_functions.test.js +124 -2
- package/src/lib_maplibre.js +26 -2
- package/src/lib_maplibre_relocalize.test.js +78 -0
- package/src/named_controls.wiring.test.js +468 -0
- package/src/nav_cards_helpers.js +9 -1
- package/src/nav_item_badge.wiring.test.js +182 -1
- package/src/shell_route_map.js +25 -8
- package/src/table_editor_name.wiring.test.js +201 -0
- package/src/treedb_column_title.wiring.test.js +151 -0
- package/src/treedb_json_drill.wiring.test.js +41 -1
- package/src/window_layer.wiring.test.js +308 -43
- package/src/window_show.wiring.test.js +6 -2
- package/src/window_stacking.wiring.test.js +334 -0
- package/src/yui_badge.js +58 -13
- package/src/yui_badge.test.js +40 -3
- package/src/yui_dev.js +2 -1
- package/src/yui_file_field.js +2 -0
- package/src/yui_frontend_view.js +2 -1
- package/src/yui_gclass_view.js +9 -6
- package/src/yui_tabulator_i18n.js +42 -0
- package/src/yui_tabulator_i18n.test.js +48 -1
|
@@ -209,6 +209,38 @@ function errors()
|
|
|
209
209
|
return logged.filter((l) => l.level === "error" || l.level === "warning").map((l) => l.msg);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/* The NAME a reader computes for a control from its aria-labelledby,
|
|
213
|
+
* as the accessible-name algorithm does: a reference to the control
|
|
214
|
+
* itself reads its aria-label, any other the text of the element it
|
|
215
|
+
* points at (a hidden one too, it is referenced directly), joined by
|
|
216
|
+
* spaces. Without one, its aria-label. */
|
|
217
|
+
function name_of($control)
|
|
218
|
+
{
|
|
219
|
+
const ids = ($control.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
|
|
220
|
+
if(ids.length === 0) {
|
|
221
|
+
return $control.getAttribute("aria-label");
|
|
222
|
+
}
|
|
223
|
+
/* An id is looked up in the control's own tree: a nav made on its
|
|
224
|
+
* own in a test is not on the page. */
|
|
225
|
+
let $top = $control;
|
|
226
|
+
while($top.parentNode) {
|
|
227
|
+
$top = $top.parentNode;
|
|
228
|
+
}
|
|
229
|
+
return ids.map((id) => {
|
|
230
|
+
if(id === $control.id) {
|
|
231
|
+
return $control.getAttribute("aria-label") || "";
|
|
232
|
+
}
|
|
233
|
+
const $el = $top.querySelector(`#${id}`);
|
|
234
|
+
expect(!!$el, `aria-labelledby points at '${id}', which is not in its tree`).toBe(true);
|
|
235
|
+
return $el.textContent;
|
|
236
|
+
}).join(" ").trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function said(shell)
|
|
240
|
+
{
|
|
241
|
+
return root(shell).querySelector(".SHELL_BADGE_STATUS").textContent;
|
|
242
|
+
}
|
|
243
|
+
|
|
212
244
|
|
|
213
245
|
describe("a badge declared in app_config.json", () => {
|
|
214
246
|
test("renders: after the label in the rail, on the icon in the icon bar", () => {
|
|
@@ -216,7 +248,7 @@ describe("a badge declared in app_config.json", () => {
|
|
|
216
248
|
|
|
217
249
|
const $rail = badge_in(zone(shell, "left"), "alarms");
|
|
218
250
|
expect(shows($rail)).toEqual({text: "2", tones: ["warning"]});
|
|
219
|
-
expect($rail.getAttribute("role")).toBe(
|
|
251
|
+
expect($rail.getAttribute("role")).toBe(null); // the shell says it now
|
|
220
252
|
expect($rail.classList.contains("yui-badge")).toBe(true);
|
|
221
253
|
expect($rail.classList.contains("yui-nav-badge-pinned")).toBe(false);
|
|
222
254
|
expect($rail.parentNode.tagName).toBe("A");
|
|
@@ -562,3 +594,152 @@ describe("the toolbar badge takes a tone too", () => {
|
|
|
562
594
|
expect(e[0]).toContain("yui_shell_set_toolbar_item_badge('bell')");
|
|
563
595
|
});
|
|
564
596
|
});
|
|
597
|
+
|
|
598
|
+
/*
|
|
599
|
+
* REV-CLOUD V23.5.5: what a READER gets. Up to 7.26.34 the badge was its
|
|
600
|
+
* own live region: the count was never part of the item's name, the
|
|
601
|
+
* region said a bare number, it was hidden while empty (so 0 -> 1 was
|
|
602
|
+
* most likely never said), and inside a <button> (the toolbar, an
|
|
603
|
+
* accordion head) it was not in the tree at all.
|
|
604
|
+
*/
|
|
605
|
+
describe("a reader gets the count with the name of what it counts", () => {
|
|
606
|
+
function control_of($badge)
|
|
607
|
+
{
|
|
608
|
+
return $badge.closest("a, button");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
test("the count is part of the item's name, in every layout that draws one", () => {
|
|
612
|
+
const shell = new_shell();
|
|
613
|
+
for(const $b of badges_in(root(shell), "alarms")) {
|
|
614
|
+
const $a = control_of($b);
|
|
615
|
+
/* rail and icon bar seed 2; the drawer's entry declares none */
|
|
616
|
+
const expected = shows($b).text ? `alarms ${shows($b).text}` : "alarms";
|
|
617
|
+
expect(name_of($a)).toBe(expected);
|
|
618
|
+
}
|
|
619
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 7);
|
|
620
|
+
for(const $b of badges_in(root(shell), "alarms")) {
|
|
621
|
+
expect(name_of(control_of($b))).toBe("alarms 7");
|
|
622
|
+
}
|
|
623
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 0);
|
|
624
|
+
for(const $b of badges_in(root(shell), "alarms")) {
|
|
625
|
+
expect(name_of(control_of($b))).toBe("alarms");
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("tabs, cards and an accordion head -- a <button> -- too", () => {
|
|
630
|
+
let $c = root(new_nav({layout: "tabs", menu_id: "lab",
|
|
631
|
+
menu_items: [{id: "t", name: "t", icon: "yi-eye", route: "/t", badge: 2}]}));
|
|
632
|
+
expect(name_of(control_of(badge_in($c, "t")))).toBe("t 2");
|
|
633
|
+
|
|
634
|
+
$c = root(new_nav({layout: "cards", menu_id: "lab",
|
|
635
|
+
menu_items: [{id: "c", name: "c", icon: "yi-eye", route: "/c", badge: 3}]}));
|
|
636
|
+
expect(name_of(control_of(badge_in($c, "c")))).toBe("c 3");
|
|
637
|
+
|
|
638
|
+
$c = root(new_nav({layout: "accordion", menu_id: "lab",
|
|
639
|
+
menu_items: [{id: "s", name: "section", route: "/s", badge: 4, submenu: {items: []}}]}));
|
|
640
|
+
const $head = $c.querySelector(".yui-accordion-head");
|
|
641
|
+
expect($head.tagName).toBe("BUTTON");
|
|
642
|
+
expect(name_of($head)).toBe("section 4");
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("the toolbar's button too", () => {
|
|
646
|
+
const shell = new_shell();
|
|
647
|
+
const $btn = root(shell).querySelector('[data-toolbar-item-id="bell"]');
|
|
648
|
+
expect($btn.tagName).toBe("BUTTON");
|
|
649
|
+
expect(name_of($btn)).toBe("alarms 4");
|
|
650
|
+
yui_shell_set_toolbar_item_badge(shell, "bell", 9);
|
|
651
|
+
expect(name_of($btn)).toBe("alarms 9");
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("the name changes language with the app, the count stays", async () => {
|
|
655
|
+
const i18next = (await import("i18next")).default;
|
|
656
|
+
i18next.addResourceBundle("es", "translation", {"alarms": "alarmas"}, true, true);
|
|
657
|
+
const shell = new_shell();
|
|
658
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 5);
|
|
659
|
+
yui_shell_set_translator(shell, i18next.getFixedT("es"));
|
|
660
|
+
yui_shell_language_changed(shell);
|
|
661
|
+
const $a = zone(shell, "left").querySelector('a[data-item-id="alarms"]');
|
|
662
|
+
expect(name_of($a)).toBe("alarmas 5");
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("one live region, always in the tree: not hidden, inside no control", () => {
|
|
666
|
+
const shell = new_shell();
|
|
667
|
+
const $status = root(shell).querySelector(".SHELL_BADGE_STATUS");
|
|
668
|
+
expect($status.getAttribute("role")).toBe("status");
|
|
669
|
+
expect($status.getAttribute("aria-live")).toBe("polite");
|
|
670
|
+
expect($status.getAttribute("aria-atomic")).toBe("true");
|
|
671
|
+
expect($status.hasAttribute("hidden")).toBe(false);
|
|
672
|
+
expect($status.classList.contains("is-sr-only")).toBe(true);
|
|
673
|
+
expect($status.closest("a, button")).toBe(null);
|
|
674
|
+
expect(said(shell)).toBe("");
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("the first count is said, with the item's name -- once, though three navs draw it", async () => {
|
|
678
|
+
const i18next = (await import("i18next")).default;
|
|
679
|
+
i18next.addResourceBundle("en", "translation",
|
|
680
|
+
{"{{name}}, {{n}}": "{{name}}, {{n}}", "alarms": "Alarms", "home": "Home"}, true, true);
|
|
681
|
+
const shell = new_shell();
|
|
682
|
+
yui_shell_set_translator(shell, i18next.getFixedT("en"));
|
|
683
|
+
|
|
684
|
+
/* "home" has no badge: its first count is a change from nothing */
|
|
685
|
+
yui_shell_set_nav_item_badge(shell, "home", 1);
|
|
686
|
+
expect(said(shell)).toBe("Home, 1");
|
|
687
|
+
|
|
688
|
+
const writes = [];
|
|
689
|
+
const $status = root(shell).querySelector(".SHELL_BADGE_STATUS");
|
|
690
|
+
const real = Object.getOwnPropertyDescriptor(Object.getPrototypeOf($status), "textContent");
|
|
691
|
+
Object.defineProperty($status, "textContent", {
|
|
692
|
+
get() {
|
|
693
|
+
return real.get.call(this);
|
|
694
|
+
},
|
|
695
|
+
set(v) {
|
|
696
|
+
writes.push(v);
|
|
697
|
+
real.set.call(this, v);
|
|
698
|
+
},
|
|
699
|
+
configurable: true
|
|
700
|
+
});
|
|
701
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 3);
|
|
702
|
+
expect(writes).toEqual(["Alarms, 3"]);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("the same count is no news, a count going away is not either, one coming back is", async () => {
|
|
706
|
+
const i18next = (await import("i18next")).default;
|
|
707
|
+
i18next.addResourceBundle("en", "translation",
|
|
708
|
+
{"{{name}}, {{n}}": "{{name}}, {{n}}", "alarms": "Alarms"}, true, true);
|
|
709
|
+
const shell = new_shell();
|
|
710
|
+
yui_shell_set_translator(shell, i18next.getFixedT("en"));
|
|
711
|
+
const $status = root(shell).querySelector(".SHELL_BADGE_STATUS");
|
|
712
|
+
|
|
713
|
+
/* The seed is 2: setting 2 changes nothing a reader sees */
|
|
714
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 2);
|
|
715
|
+
expect(said(shell)).toBe("");
|
|
716
|
+
|
|
717
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 3);
|
|
718
|
+
expect(said(shell)).toBe("Alarms, 3");
|
|
719
|
+
const node = $status.firstChild;
|
|
720
|
+
|
|
721
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 3, "danger"); // only the tone
|
|
722
|
+
expect($status.firstChild).toBe(node);
|
|
723
|
+
|
|
724
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 0);
|
|
725
|
+
expect($status.firstChild).toBe(node); // not said
|
|
726
|
+
|
|
727
|
+
yui_shell_set_nav_item_badge(shell, "alarms", 3);
|
|
728
|
+
expect(said(shell)).toBe("Alarms, 3");
|
|
729
|
+
expect($status.firstChild).not.toBe(node); // said again
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("the toolbar's count is said with the button's name", async () => {
|
|
733
|
+
const i18next = (await import("i18next")).default;
|
|
734
|
+
i18next.addResourceBundle("en", "translation",
|
|
735
|
+
{"{{name}}, {{n}}": "{{name}}, {{n}}", "alarms": "Alarms"}, true, true);
|
|
736
|
+
const shell = new_shell();
|
|
737
|
+
yui_shell_set_translator(shell, i18next.getFixedT("en"));
|
|
738
|
+
|
|
739
|
+
yui_shell_set_toolbar_item_badge(shell, "bell", 4); // the seed: no news
|
|
740
|
+
expect(said(shell)).toBe("");
|
|
741
|
+
yui_shell_set_toolbar_item_badge(shell, "bell", 120);
|
|
742
|
+
expect(said(shell)).toBe("Alarms, 99+");
|
|
743
|
+
expect(errors()).toEqual([]);
|
|
744
|
+
});
|
|
745
|
+
});
|
package/src/shell_route_map.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
log_error,
|
|
33
33
|
} from "@yuneta/gobj-js";
|
|
34
34
|
|
|
35
|
-
import {yui_shell_show_modal
|
|
35
|
+
import {yui_shell_show_modal} from "./shell_modals.js";
|
|
36
36
|
import {yui_shell_nav_map} from "./c_yui_shell.js";
|
|
37
37
|
import {attach_clear} from "./yui_inputs.js";
|
|
38
38
|
|
|
@@ -86,7 +86,7 @@ function render_node(node)
|
|
|
86
86
|
let row = [];
|
|
87
87
|
if(node.icon && /^yi-[a-z0-9-]+$/.test(node.icon)) {
|
|
88
88
|
row.push(["span", {class: "icon ROUTEMAP_ICON"},
|
|
89
|
-
[["i", {class: node.icon}]]]);
|
|
89
|
+
[["i", {class: node.icon, "aria-hidden": "true"}]]]);
|
|
90
90
|
}
|
|
91
91
|
row.push(["span", {class: "ROUTEMAP_NAME", i18n: node.label},
|
|
92
92
|
text_node(node.label)]);
|
|
@@ -123,10 +123,22 @@ function render_node(node)
|
|
|
123
123
|
(node.ref ? " ROUTEMAP_IS_REF" : "");
|
|
124
124
|
let $row;
|
|
125
125
|
if(node.route) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
/* Its NAME is the node's label, with its key (build_body()
|
|
127
|
+
* translates it with the rest): taken from its content, a link
|
|
128
|
+
* read the label, the route, the gclass, the event and "shown
|
|
129
|
+
* above" as one name. The title stays the route, data shown
|
|
130
|
+
* where the reader points; "you are here" is aria-current. */
|
|
131
|
+
let a_attrs = {class: "ROUTEMAP_LINK ROUTEMAP_ROW" + row_class,
|
|
132
|
+
href: "#" + node.route,
|
|
133
|
+
title: node.route};
|
|
134
|
+
if(node.label) {
|
|
135
|
+
a_attrs["aria-label"] = node.label;
|
|
136
|
+
a_attrs["data-i18n-aria-label"] = node.label;
|
|
137
|
+
}
|
|
138
|
+
if(node.current) {
|
|
139
|
+
a_attrs["aria-current"] = "page";
|
|
140
|
+
}
|
|
141
|
+
$row = createElement2(["a", a_attrs, row]);
|
|
130
142
|
} else {
|
|
131
143
|
$row = createElement2(
|
|
132
144
|
["span", {class: "ROUTEMAP_ROW ROUTEMAP_STRUCT" + row_class}, row]);
|
|
@@ -497,8 +509,13 @@ export function yui_shell_show_route_map(shell, opts)
|
|
|
497
509
|
* `keep_on_navigate` gives it the same survives-navigation
|
|
498
510
|
* behaviour there. */
|
|
499
511
|
let $body = build_body(shell, t);
|
|
500
|
-
|
|
501
|
-
|
|
512
|
+
/* The WINDOW layer, where every window lives (the legacy
|
|
513
|
+
* '#top-layer' when a page has one): no `$parent` takes it. It was
|
|
514
|
+
* the popup layer, above the drawer and in the way of the
|
|
515
|
+
* toolbar's dropdowns, which opened BEHIND the map where they
|
|
516
|
+
* met it, and in an order of its own that no other window could
|
|
517
|
+
* rise above. */
|
|
518
|
+
let $parent = (typeof document !== "undefined" &&
|
|
502
519
|
document.getElementById("top-layer")) || null;
|
|
503
520
|
let win = gobj_create_service(WIN_NAME, "C_YUI_WINDOW", {
|
|
504
521
|
$parent: $parent,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* table_editor_name.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* The box Tabulator draws in a cell while it is edited had no
|
|
5
|
+
* name: no label, no aria-label, no title (REV-CLOUD V23.5.6, seen
|
|
6
|
+
* in the alarms of an estadodelaire device). Both tables of the
|
|
7
|
+
* library that edit in place -- a table field of C_YUI_FORM and
|
|
8
|
+
* the inline editing of C_YUI_TREEDB_TOPIC_WITH_FORM -- now name
|
|
9
|
+
* it after its column, asked at the moment of editing: Tabulator
|
|
10
|
+
* calls a function `editorParams` with the cell, and its built-in
|
|
11
|
+
* editors write the `elementAttributes` it answers.
|
|
12
|
+
*
|
|
13
|
+
* Driven through the real gclasses on a document double, with a
|
|
14
|
+
* Tabulator double that keeps the settings it was built with.
|
|
15
|
+
*
|
|
16
|
+
* Copyright (c) 2026, ArtGins.
|
|
17
|
+
* All Rights Reserved.
|
|
18
|
+
***********************************************************************/
|
|
19
|
+
import {describe, test, expect, beforeAll, afterEach, vi} from "vitest";
|
|
20
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
21
|
+
|
|
22
|
+
install_dom_double();
|
|
23
|
+
|
|
24
|
+
if(typeof globalThis.ResizeObserver === "undefined") {
|
|
25
|
+
globalThis.ResizeObserver = class {
|
|
26
|
+
observe() {}
|
|
27
|
+
unobserve() {}
|
|
28
|
+
disconnect() {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tables = vi.hoisted(() => []);
|
|
33
|
+
vi.mock("tabulator-tables", () => {
|
|
34
|
+
class FakeTabulator {
|
|
35
|
+
constructor($el, settings) {
|
|
36
|
+
this.$el = $el;
|
|
37
|
+
this.element = $el;
|
|
38
|
+
this.settings = settings;
|
|
39
|
+
this.options = settings;
|
|
40
|
+
this.externalEvents = {events: {}};
|
|
41
|
+
this.initialized = true;
|
|
42
|
+
tables.push(this);
|
|
43
|
+
return new Proxy(this, {
|
|
44
|
+
get(target, prop) {
|
|
45
|
+
if(prop in target) {
|
|
46
|
+
return target[prop];
|
|
47
|
+
}
|
|
48
|
+
return () => [];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
on(ev, fn) {
|
|
53
|
+
(this.externalEvents.events[ev] = this.externalEvents.events[ev] || []).push(fn);
|
|
54
|
+
}
|
|
55
|
+
off(ev) {
|
|
56
|
+
delete this.externalEvents.events[ev];
|
|
57
|
+
}
|
|
58
|
+
getRows() {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
destroy() {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {TabulatorFull: FakeTabulator, Tabulator: FakeTabulator};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/* No json field is opened here, but the form imports the editor. */
|
|
68
|
+
vi.mock("vanilla-jsoneditor", () => ({
|
|
69
|
+
createJSONEditor: () => ({
|
|
70
|
+
get() {
|
|
71
|
+
return {text: ""};
|
|
72
|
+
},
|
|
73
|
+
set() {},
|
|
74
|
+
destroy() {},
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const {
|
|
79
|
+
SDATA_END, gclass_create,
|
|
80
|
+
gobj_start_up, gobj_create_yuno, gobj_create_pure_child, gobj_create_service,
|
|
81
|
+
gobj_start, gobj_write_bool_attr,
|
|
82
|
+
} = await import("@yuneta/gobj-js");
|
|
83
|
+
const {register_c_yui_form} = await import("./c_yui_form.js");
|
|
84
|
+
const {register_c_yui_treedb_topic_with_form} = await import("./c_yui_treedb_topic_with_form.js");
|
|
85
|
+
|
|
86
|
+
let i18next = null;
|
|
87
|
+
let yuno = null;
|
|
88
|
+
let seq = 0;
|
|
89
|
+
|
|
90
|
+
beforeAll(async () => {
|
|
91
|
+
i18next = (await import("i18next")).default;
|
|
92
|
+
await i18next.init({
|
|
93
|
+
lng: "en",
|
|
94
|
+
resources: {
|
|
95
|
+
en: {translation: {"threshold": "Threshold", "users.name": "User name"}},
|
|
96
|
+
es: {translation: {"threshold": "Umbral", "users.name": "Nombre del usuario"}}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
100
|
+
gclass_create("C_TEST_EDITOR_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
101
|
+
gclass_create(
|
|
102
|
+
"C_TEST_EDITOR_APP",
|
|
103
|
+
[["EV_RECORD_CHANGED", 0]],
|
|
104
|
+
[["ST_IDLE", [["EV_RECORD_CHANGED", () => 0, null]]]],
|
|
105
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
106
|
+
);
|
|
107
|
+
register_c_yui_form();
|
|
108
|
+
register_c_yui_treedb_topic_with_form();
|
|
109
|
+
yuno = gobj_create_yuno("editor_name_yuno", "C_TEST_EDITOR_HOST", {});
|
|
110
|
+
gobj_start(yuno);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(async () => {
|
|
114
|
+
tables.length = 0;
|
|
115
|
+
await i18next.changeLanguage("en");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function column(table, field)
|
|
119
|
+
{
|
|
120
|
+
return (table.settings.columns || []).find((c) => c.field === field);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* What Tabulator hands the editor when a cell of `col` starts editing. */
|
|
124
|
+
function editor_params(col)
|
|
125
|
+
{
|
|
126
|
+
return (typeof col.editorParams === "function")
|
|
127
|
+
? col.editorParams({getValue: () => ""})
|
|
128
|
+
: (col.editorParams || {});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("a table field of C_YUI_FORM", () => {
|
|
132
|
+
test("names the editor of an editable cell after its column, in the language of the moment", async () => {
|
|
133
|
+
const app = gobj_create_pure_child(`eapp${++seq}`, "C_TEST_EDITOR_APP", {}, yuno);
|
|
134
|
+
gobj_start(app);
|
|
135
|
+
const form = gobj_create_pure_child(`eform${seq}`, "C_YUI_FORM", {
|
|
136
|
+
template: {
|
|
137
|
+
triggers: {
|
|
138
|
+
type: "array",
|
|
139
|
+
flag: ["persistent", "writable", "table"],
|
|
140
|
+
table: {
|
|
141
|
+
id: {type: "string", flag: ["writable", "persistent", "required"]},
|
|
142
|
+
value: {type: "string", header: "threshold", flag: ["writable", "persistent"]},
|
|
143
|
+
note: {type: "string", flag: ["persistent"]} // not writable
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
record: {triggers: []},
|
|
148
|
+
editable: true,
|
|
149
|
+
}, app);
|
|
150
|
+
gobj_start(form);
|
|
151
|
+
expect(tables.length).toBe(1);
|
|
152
|
+
|
|
153
|
+
const value = column(tables[0], "value");
|
|
154
|
+
expect(value.editor).toBe("input");
|
|
155
|
+
expect(editor_params(value).elementAttributes).toEqual(
|
|
156
|
+
{"aria-label": "Threshold", title: "Threshold"});
|
|
157
|
+
|
|
158
|
+
await i18next.changeLanguage("es");
|
|
159
|
+
expect(editor_params(value).elementAttributes).toEqual(
|
|
160
|
+
{"aria-label": "Umbral", title: "Umbral"});
|
|
161
|
+
|
|
162
|
+
/* A column nobody edits has no editor to name: what params it
|
|
163
|
+
* had for one are left as they were. */
|
|
164
|
+
const note = column(tables[0], "note");
|
|
165
|
+
expect(note.editor).toBe(undefined);
|
|
166
|
+
expect(typeof note.editorParams).not.toBe("function");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("the inline editing of C_YUI_TREEDB_TOPIC_WITH_FORM", () => {
|
|
171
|
+
test("names the editor after the header's own cascade, in the language of the moment", async () => {
|
|
172
|
+
const form = gobj_create_service(`etopic${++seq}`, "C_YUI_TREEDB_TOPIC_WITH_FORM", {
|
|
173
|
+
treedb_name: "treedb_test",
|
|
174
|
+
topic_name: "users",
|
|
175
|
+
desc: {
|
|
176
|
+
id: "users",
|
|
177
|
+
pkey: "id",
|
|
178
|
+
cols: [
|
|
179
|
+
{id: "id", header: "Id", type: "string", flag: ["persistent", "required"]},
|
|
180
|
+
{id: "name", header: "Name", type: "string", flag: ["persistent", "writable"]}
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
}, yuno);
|
|
184
|
+
gobj_write_bool_attr(form, "editable", true);
|
|
185
|
+
gobj_start(form);
|
|
186
|
+
expect(tables.length > 0).toBe(true);
|
|
187
|
+
const table = tables[tables.length - 1];
|
|
188
|
+
|
|
189
|
+
const name = column(table, "name");
|
|
190
|
+
expect(name.editor).toBe("input");
|
|
191
|
+
expect(editor_params(name).elementAttributes).toEqual(
|
|
192
|
+
{"aria-label": "User name", title: "User name"});
|
|
193
|
+
|
|
194
|
+
await i18next.changeLanguage("es");
|
|
195
|
+
expect(editor_params(name).elementAttributes).toEqual(
|
|
196
|
+
{"aria-label": "Nombre del usuario", title: "Nombre del usuario"});
|
|
197
|
+
|
|
198
|
+
/* The pkey is never edited in place. */
|
|
199
|
+
expect(column(table, "id").editor).toBe(undefined);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_column_title.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* The header of a column of C_YUI_TREEDB_TOPIC_WITH_FORM is
|
|
5
|
+
* translated by a CASCADE -- '<topic>.<col>', then '<col>', then
|
|
6
|
+
* the schema's header -- and carries the key a language change
|
|
7
|
+
* re-translates it by (data-i18n). Up to 7.26.34 that key was
|
|
8
|
+
* always the bare '<col>', whatever key had translated it:
|
|
9
|
+
*
|
|
10
|
+
* (a) a header translated by '<topic>.<col>' alone carried no
|
|
11
|
+
* key, and stayed in its language;
|
|
12
|
+
* (b) one translated by both went to the bare column's text.
|
|
13
|
+
*
|
|
14
|
+
* The apps with a shell did not show it: the view rebuilds its
|
|
15
|
+
* columns on EV_LANGUAGE_CHANGED, after refresh_language(), and
|
|
16
|
+
* the formatter runs the cascade again. A refresh_language() alone
|
|
17
|
+
* did. The twin of C_YUI_FORM's field labels
|
|
18
|
+
* (form_label_i18n.wiring.test.js).
|
|
19
|
+
*
|
|
20
|
+
* The real gclass on a document double, with a Tabulator double
|
|
21
|
+
* that keeps the columns it was built with.
|
|
22
|
+
*
|
|
23
|
+
* Copyright (c) 2026, ArtGins.
|
|
24
|
+
* All Rights Reserved.
|
|
25
|
+
***********************************************************************/
|
|
26
|
+
import {describe, test, expect, beforeAll, afterEach, vi} from "vitest";
|
|
27
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
28
|
+
|
|
29
|
+
install_dom_double();
|
|
30
|
+
|
|
31
|
+
if(typeof globalThis.ResizeObserver === "undefined") {
|
|
32
|
+
globalThis.ResizeObserver = class {
|
|
33
|
+
observe() {}
|
|
34
|
+
unobserve() {}
|
|
35
|
+
disconnect() {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tables = vi.hoisted(() => []);
|
|
40
|
+
vi.mock("tabulator-tables", () => {
|
|
41
|
+
class FakeTabulator {
|
|
42
|
+
constructor($el, settings) {
|
|
43
|
+
this.element = $el;
|
|
44
|
+
this.options = settings;
|
|
45
|
+
this.externalEvents = {events: {}};
|
|
46
|
+
tables.push(this);
|
|
47
|
+
return new Proxy(this, {
|
|
48
|
+
get(target, prop) {
|
|
49
|
+
if(prop in target) {
|
|
50
|
+
return target[prop];
|
|
51
|
+
}
|
|
52
|
+
return () => [];
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
on(ev, fn) {
|
|
57
|
+
(this.externalEvents.events[ev] = this.externalEvents.events[ev] || []).push(fn);
|
|
58
|
+
}
|
|
59
|
+
off(ev) {
|
|
60
|
+
delete this.externalEvents.events[ev];
|
|
61
|
+
}
|
|
62
|
+
destroy() {}
|
|
63
|
+
}
|
|
64
|
+
return {TabulatorFull: FakeTabulator, Tabulator: FakeTabulator};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const {
|
|
68
|
+
SDATA_END, gclass_create,
|
|
69
|
+
gobj_start_up, gobj_create_yuno, gobj_create_service, gobj_start,
|
|
70
|
+
} = await import("@yuneta/gobj-js");
|
|
71
|
+
const {register_c_yui_treedb_topic_with_form} = await import("./c_yui_treedb_topic_with_form.js");
|
|
72
|
+
|
|
73
|
+
let i18next = null;
|
|
74
|
+
let yuno = null;
|
|
75
|
+
let seq = 0;
|
|
76
|
+
|
|
77
|
+
beforeAll(async () => {
|
|
78
|
+
i18next = (await import("i18next")).default;
|
|
79
|
+
await i18next.init({
|
|
80
|
+
lng: "en",
|
|
81
|
+
resources: {
|
|
82
|
+
en: {translation: {
|
|
83
|
+
"users.alias": "User alias", // (a)
|
|
84
|
+
"users.title": "User title", "title": "Title", // (b)
|
|
85
|
+
"email": "E-mail" // the column alone
|
|
86
|
+
}},
|
|
87
|
+
es: {translation: {
|
|
88
|
+
"users.alias": "Alias del usuario",
|
|
89
|
+
"users.title": "Título del usuario", "title": "Título",
|
|
90
|
+
"email": "Correo"
|
|
91
|
+
}}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
95
|
+
gclass_create("C_TEST_TITLE_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
96
|
+
register_c_yui_treedb_topic_with_form();
|
|
97
|
+
yuno = gobj_create_yuno("column_title_yuno", "C_TEST_TITLE_HOST", {});
|
|
98
|
+
gobj_start(yuno);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(async () => {
|
|
102
|
+
tables.length = 0;
|
|
103
|
+
await i18next.changeLanguage("en");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/* The header a column draws, as Tabulator asks its formatter. */
|
|
107
|
+
function header_of(field)
|
|
108
|
+
{
|
|
109
|
+
const view = gobj_create_service(`title_view${++seq}`, "C_YUI_TREEDB_TOPIC_WITH_FORM", {
|
|
110
|
+
treedb_name: "treedb_test",
|
|
111
|
+
topic_name: "users",
|
|
112
|
+
desc: {id: "users", pkey: "id", cols: [
|
|
113
|
+
{id: "id", header: "Id", type: "string", flag: ["persistent", "required"]},
|
|
114
|
+
{id: field, header: field, type: "string", flag: ["persistent"]}
|
|
115
|
+
]}
|
|
116
|
+
}, yuno);
|
|
117
|
+
gobj_start(view);
|
|
118
|
+
const col = tables[tables.length - 1].options.columns.find((c) => c.field === field);
|
|
119
|
+
expect(typeof col.titleFormatter).toBe("function");
|
|
120
|
+
return col.titleFormatter({});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe("the header of a column changes language by the key that translated it", () => {
|
|
124
|
+
test("(a) translated by '<topic>.<col>' alone", async () => {
|
|
125
|
+
const $h = header_of("alias");
|
|
126
|
+
expect($h.textContent).toBe("User alias");
|
|
127
|
+
expect($h.getAttribute("data-i18n")).toBe("users.alias");
|
|
128
|
+
await i18next.changeLanguage("es");
|
|
129
|
+
expect(i18next.t($h.getAttribute("data-i18n"))).toBe("Alias del usuario");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("(b) translated by both: the topic's key, before and after", async () => {
|
|
133
|
+
const $h = header_of("title");
|
|
134
|
+
expect($h.textContent).toBe("User title");
|
|
135
|
+
expect($h.getAttribute("data-i18n")).toBe("users.title");
|
|
136
|
+
await i18next.changeLanguage("es");
|
|
137
|
+
expect(i18next.t($h.getAttribute("data-i18n"))).toBe("Título del usuario");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("translated by '<col>' alone, as before", () => {
|
|
141
|
+
const $h = header_of("email");
|
|
142
|
+
expect($h.textContent).toBe("E-mail");
|
|
143
|
+
expect($h.getAttribute("data-i18n")).toBe("email");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("translated by nothing: the schema's header, with no key, as before", () => {
|
|
147
|
+
const $h = header_of("zone");
|
|
148
|
+
expect($h.textContent).toBe("zone");
|
|
149
|
+
expect($h.getAttribute("data-i18n")).toBe(null);
|
|
150
|
+
});
|
|
151
|
+
});
|