@yuneta/gobj-ui 7.26.33 → 7.26.34
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/dist/gobj-ui.cjs.js
CHANGED
|
@@ -10415,6 +10415,7 @@ function template2columns(gobj, columns, template, sub_elements) {
|
|
|
10415
10415
|
else return value;
|
|
10416
10416
|
};
|
|
10417
10417
|
if (field_desc.is_required) validator.push("required");
|
|
10418
|
+
if ((0, _yuneta_gobj_js.is_object)(value) && value.validator) validator.push(...Array.isArray(value.validator) ? value.validator : [value.validator]);
|
|
10418
10419
|
switch (field_desc.type) {
|
|
10419
10420
|
case "object":
|
|
10420
10421
|
case "dict": break;
|
|
@@ -10631,12 +10632,20 @@ function validate_form(gobj, $form) {
|
|
|
10631
10632
|
mark_field_validity($editor);
|
|
10632
10633
|
}
|
|
10633
10634
|
});
|
|
10635
|
+
let $first_bad = null;
|
|
10636
|
+
function table_valid(tabulator) {
|
|
10637
|
+
let r = tabulator.validate();
|
|
10638
|
+
if (r === true) return true;
|
|
10639
|
+
if (!$first_bad && Array.isArray(r) && r.length > 0) $first_bad = r[0].getElement();
|
|
10640
|
+
return false;
|
|
10641
|
+
}
|
|
10634
10642
|
$form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
|
|
10635
|
-
if (
|
|
10643
|
+
if (!table_valid($table.tabulator)) validated = false;
|
|
10636
10644
|
$table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
|
|
10637
|
-
if (
|
|
10645
|
+
if (!table_valid($table_nested.tabulator)) validated = false;
|
|
10638
10646
|
});
|
|
10639
10647
|
});
|
|
10648
|
+
if ($first_bad) $first_bad.scrollIntoView({ block: "nearest" });
|
|
10640
10649
|
return validated;
|
|
10641
10650
|
}
|
|
10642
10651
|
/************************************************************
|
|
@@ -10756,7 +10765,13 @@ function clear_data(gobj, $form) {
|
|
|
10756
10765
|
} else if ($input.type === "checkbox") $input.checked = false;
|
|
10757
10766
|
else if ($input.type === "radio") $input.checked = false;
|
|
10758
10767
|
else if ($input.type === "datetime-local") $input.value = null;
|
|
10759
|
-
else if ($input.type === "
|
|
10768
|
+
else if ($input.type === "color") {
|
|
10769
|
+
$input.value = "#000000";
|
|
10770
|
+
if ($input.yui_color_value) {
|
|
10771
|
+
$input.yui_color_value.textContent = $input.value;
|
|
10772
|
+
$input.yui_color_value.title = $input.value;
|
|
10773
|
+
}
|
|
10774
|
+
} else if ($input.type === "select-multiple") $input.tom_select.clear();
|
|
10760
10775
|
else {
|
|
10761
10776
|
$input.value = null;
|
|
10762
10777
|
if ($input.yui_svg_preview) $input.yui_svg_preview();
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -10410,6 +10410,7 @@ function template2columns(gobj, columns, template, sub_elements) {
|
|
|
10410
10410
|
else return value;
|
|
10411
10411
|
};
|
|
10412
10412
|
if (field_desc.is_required) validator.push("required");
|
|
10413
|
+
if (is_object(value) && value.validator) validator.push(...Array.isArray(value.validator) ? value.validator : [value.validator]);
|
|
10413
10414
|
switch (field_desc.type) {
|
|
10414
10415
|
case "object":
|
|
10415
10416
|
case "dict": break;
|
|
@@ -10626,12 +10627,20 @@ function validate_form(gobj, $form) {
|
|
|
10626
10627
|
mark_field_validity($editor);
|
|
10627
10628
|
}
|
|
10628
10629
|
});
|
|
10630
|
+
let $first_bad = null;
|
|
10631
|
+
function table_valid(tabulator) {
|
|
10632
|
+
let r = tabulator.validate();
|
|
10633
|
+
if (r === true) return true;
|
|
10634
|
+
if (!$first_bad && Array.isArray(r) && r.length > 0) $first_bad = r[0].getElement();
|
|
10635
|
+
return false;
|
|
10636
|
+
}
|
|
10629
10637
|
$form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
|
|
10630
|
-
if (
|
|
10638
|
+
if (!table_valid($table.tabulator)) validated = false;
|
|
10631
10639
|
$table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
|
|
10632
|
-
if (
|
|
10640
|
+
if (!table_valid($table_nested.tabulator)) validated = false;
|
|
10633
10641
|
});
|
|
10634
10642
|
});
|
|
10643
|
+
if ($first_bad) $first_bad.scrollIntoView({ block: "nearest" });
|
|
10635
10644
|
return validated;
|
|
10636
10645
|
}
|
|
10637
10646
|
/************************************************************
|
|
@@ -10751,7 +10760,13 @@ function clear_data(gobj, $form) {
|
|
|
10751
10760
|
} else if ($input.type === "checkbox") $input.checked = false;
|
|
10752
10761
|
else if ($input.type === "radio") $input.checked = false;
|
|
10753
10762
|
else if ($input.type === "datetime-local") $input.value = null;
|
|
10754
|
-
else if ($input.type === "
|
|
10763
|
+
else if ($input.type === "color") {
|
|
10764
|
+
$input.value = "#000000";
|
|
10765
|
+
if ($input.yui_color_value) {
|
|
10766
|
+
$input.yui_color_value.textContent = $input.value;
|
|
10767
|
+
$input.yui_color_value.title = $input.value;
|
|
10768
|
+
}
|
|
10769
|
+
} else if ($input.type === "select-multiple") $input.tom_select.clear();
|
|
10755
10770
|
else {
|
|
10756
10771
|
$input.value = null;
|
|
10757
10772
|
if ($input.yui_svg_preview) $input.yui_svg_preview();
|
package/package.json
CHANGED
package/src/c_yui_form.js
CHANGED
|
@@ -2273,6 +2273,15 @@ function template2columns(gobj, columns, template, sub_elements)
|
|
|
2273
2273
|
validator.push("required");
|
|
2274
2274
|
}
|
|
2275
2275
|
|
|
2276
|
+
/* What a host knows and a schema cannot say: a template item may
|
|
2277
|
+
* carry Tabulator validators of its own -- a validator's name, or
|
|
2278
|
+
* a function of (cell, value, parameters) -- that its column runs
|
|
2279
|
+
* besides `required`. A template that comes as json carries none;
|
|
2280
|
+
* the host adds them to its copy before handing it on. */
|
|
2281
|
+
if(is_object(value) && value.validator) {
|
|
2282
|
+
validator.push(...(Array.isArray(value.validator)? value.validator : [value.validator]));
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2276
2285
|
// TODO add the validators needed, like validator.push("integer");
|
|
2277
2286
|
|
|
2278
2287
|
switch(field_desc.type) {
|
|
@@ -2658,18 +2667,38 @@ function validate_form(gobj, $form)
|
|
|
2658
2667
|
}
|
|
2659
2668
|
});
|
|
2660
2669
|
|
|
2670
|
+
/* Tabulator's validate() answers `true`, or the LIST of the cells that
|
|
2671
|
+
* failed -- and a list is truthy, so `!validate()` never refused a
|
|
2672
|
+
* table: a cell left empty in a `required` column was saved. Tabulator
|
|
2673
|
+
* marks every bad cell; the first one is brought into view. */
|
|
2674
|
+
let $first_bad = null;
|
|
2675
|
+
function table_valid(tabulator) {
|
|
2676
|
+
let r = tabulator.validate();
|
|
2677
|
+
if(r === true) {
|
|
2678
|
+
return true;
|
|
2679
|
+
}
|
|
2680
|
+
if(!$first_bad && Array.isArray(r) && r.length > 0) {
|
|
2681
|
+
$first_bad = r[0].getElement();
|
|
2682
|
+
}
|
|
2683
|
+
return false;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2661
2686
|
$form.querySelectorAll('.yui-tabulator-table').forEach($table => {
|
|
2662
|
-
if(
|
|
2687
|
+
if(!table_valid($table.tabulator)) {
|
|
2663
2688
|
validated = false;
|
|
2664
2689
|
}
|
|
2665
2690
|
|
|
2666
2691
|
$table.querySelectorAll('.yui-tabulator-table-nested').forEach($table_nested => {
|
|
2667
|
-
if(
|
|
2692
|
+
if(!table_valid($table_nested.tabulator)) {
|
|
2668
2693
|
validated = false;
|
|
2669
2694
|
}
|
|
2670
2695
|
});
|
|
2671
2696
|
});
|
|
2672
2697
|
|
|
2698
|
+
if($first_bad) {
|
|
2699
|
+
$first_bad.scrollIntoView({block: "nearest"});
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2673
2702
|
return validated;
|
|
2674
2703
|
}
|
|
2675
2704
|
|
|
@@ -2851,6 +2880,18 @@ function clear_data(gobj, $form)
|
|
|
2851
2880
|
} else if($input.type === 'datetime-local') { /* time */
|
|
2852
2881
|
$input.value = null;
|
|
2853
2882
|
|
|
2883
|
+
} else if($input.type === 'color') {
|
|
2884
|
+
/* A colour input holds a colour: the "" a null is written
|
|
2885
|
+
* as is refused, the browser keeps its black, and Chrome
|
|
2886
|
+
* says so on the console, one line per field each time
|
|
2887
|
+
* the form is filled. Black, then, and the readout beside
|
|
2888
|
+
* the swatch told (writing `.value` fires nothing). */
|
|
2889
|
+
$input.value = "#000000";
|
|
2890
|
+
if($input.yui_color_value) {
|
|
2891
|
+
$input.yui_color_value.textContent = $input.value;
|
|
2892
|
+
$input.yui_color_value.title = $input.value;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2854
2895
|
} else if($input.type === 'select-multiple') {
|
|
2855
2896
|
$input.tom_select.clear();
|
|
2856
2897
|
} else {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_table_validator.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* A table field of a C_YUI_FORM ('exec' mode) and its validators.
|
|
5
|
+
*
|
|
6
|
+
* - A template item may carry Tabulator validators of its own
|
|
7
|
+
* (`validator`: a name or a function), which its column runs
|
|
8
|
+
* besides `required`: what a host knows and a schema cannot say
|
|
9
|
+
* (estadodelaire refuses a trigger value that its measure can
|
|
10
|
+
* never match).
|
|
11
|
+
* - A table that does not validate refuses the save. Tabulator's
|
|
12
|
+
* validate() answers `true` or the LIST of the bad cells, and
|
|
13
|
+
* validate_form() asked `!validate()`, which a list never is: a
|
|
14
|
+
* `required` cell left empty was saved.
|
|
15
|
+
*
|
|
16
|
+
* Driven through the real gclass on a document double, with a
|
|
17
|
+
* Tabulator double that keeps its settings and answers validate()
|
|
18
|
+
* as told.
|
|
19
|
+
*
|
|
20
|
+
* Copyright (c) 2026, ArtGins.
|
|
21
|
+
* All Rights Reserved.
|
|
22
|
+
***********************************************************************/
|
|
23
|
+
import {describe, test, expect, beforeAll, beforeEach, vi} from "vitest";
|
|
24
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
25
|
+
|
|
26
|
+
install_dom_double();
|
|
27
|
+
|
|
28
|
+
if(typeof globalThis.ResizeObserver === "undefined") {
|
|
29
|
+
globalThis.ResizeObserver = class {
|
|
30
|
+
observe() {}
|
|
31
|
+
unobserve() {}
|
|
32
|
+
disconnect() {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tables = vi.hoisted(() => []);
|
|
37
|
+
vi.mock("tabulator-tables", () => {
|
|
38
|
+
class FakeTabulator {
|
|
39
|
+
constructor($el, settings) {
|
|
40
|
+
this.$el = $el;
|
|
41
|
+
this.settings = settings;
|
|
42
|
+
this.externalEvents = {events: {}};
|
|
43
|
+
this.initialized = true;
|
|
44
|
+
this.answer = true; // what validate() says
|
|
45
|
+
this.validated = 0;
|
|
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
|
+
validate() {
|
|
63
|
+
this.validated++;
|
|
64
|
+
return this.answer;
|
|
65
|
+
}
|
|
66
|
+
getRows() {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
destroy() {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {TabulatorFull: FakeTabulator, Tabulator: FakeTabulator};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const {
|
|
76
|
+
SDATA_END, gclass_create,
|
|
77
|
+
gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
|
|
78
|
+
gobj_start, gobj_send_event,
|
|
79
|
+
set_log_callback,
|
|
80
|
+
} = await import("@yuneta/gobj-js");
|
|
81
|
+
const {register_c_yui_form} = await import("./c_yui_form.js");
|
|
82
|
+
|
|
83
|
+
const saved = [];
|
|
84
|
+
let yuno = null;
|
|
85
|
+
|
|
86
|
+
beforeAll(async () => {
|
|
87
|
+
const i18next = (await import("i18next")).default;
|
|
88
|
+
await i18next.init({lng: "en", resources: {}});
|
|
89
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
90
|
+
set_log_callback(() => {});
|
|
91
|
+
gclass_create("C_TEST_FORM_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
92
|
+
gclass_create(
|
|
93
|
+
"C_TEST_FORM_APP",
|
|
94
|
+
[["EV_SAVE_RECORD", 0], ["EV_RECORD_CHANGED", 0]],
|
|
95
|
+
[["ST_IDLE", [
|
|
96
|
+
["EV_SAVE_RECORD", (gobj, event, kw) => {
|
|
97
|
+
saved.push(kw);
|
|
98
|
+
return 0;
|
|
99
|
+
}, null],
|
|
100
|
+
["EV_RECORD_CHANGED", () => 0, null],
|
|
101
|
+
]]],
|
|
102
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
103
|
+
);
|
|
104
|
+
register_c_yui_form();
|
|
105
|
+
yuno = gobj_create_yuno("form_validator_yuno", "C_TEST_FORM_HOST", {});
|
|
106
|
+
gobj_start(yuno);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
saved.length = 0;
|
|
111
|
+
tables.length = 0;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let seq = 0;
|
|
115
|
+
|
|
116
|
+
/* The alarms of an estadodelaire device, cut down: a table whose
|
|
117
|
+
* `value` column carries a validator the host added. */
|
|
118
|
+
function build(validator)
|
|
119
|
+
{
|
|
120
|
+
const value = {
|
|
121
|
+
type: "string",
|
|
122
|
+
flag: ["writable", "persistent"]
|
|
123
|
+
};
|
|
124
|
+
if(validator) {
|
|
125
|
+
value.validator = validator;
|
|
126
|
+
}
|
|
127
|
+
const app = gobj_create_pure_child(`vapp${++seq}`, "C_TEST_FORM_APP", {}, yuno);
|
|
128
|
+
gobj_start(app);
|
|
129
|
+
const form = gobj_create_pure_child(`vform${seq}`, "C_YUI_FORM", {
|
|
130
|
+
template: {
|
|
131
|
+
triggers: {
|
|
132
|
+
type: "array",
|
|
133
|
+
flag: ["persistent", "writable", "table"],
|
|
134
|
+
table: {
|
|
135
|
+
id: {
|
|
136
|
+
type: "string",
|
|
137
|
+
flag: ["writable", "persistent", "required"]
|
|
138
|
+
},
|
|
139
|
+
value: value
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
record: {triggers: []},
|
|
144
|
+
editable: true,
|
|
145
|
+
}, app);
|
|
146
|
+
gobj_start(form);
|
|
147
|
+
expect(tables.length).toBe(1);
|
|
148
|
+
return {form, table: tables[0]};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function column(table, field)
|
|
152
|
+
{
|
|
153
|
+
return table.settings.columns.find((c) => c.field === field);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe("a table field's validators", () => {
|
|
157
|
+
test("a template item's validator runs in its column, besides required", () => {
|
|
158
|
+
const fits = () => true;
|
|
159
|
+
const {table} = build(fits);
|
|
160
|
+
expect(column(table, "id").validator).toEqual(["required"]);
|
|
161
|
+
expect(column(table, "value").validator).toEqual([fits]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a list of them, names and functions, is taken whole", () => {
|
|
165
|
+
const fits = () => true;
|
|
166
|
+
const {table} = build(["numeric", fits]);
|
|
167
|
+
expect(column(table, "value").validator).toEqual(["numeric", fits]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("an item without one keeps what the schema says", () => {
|
|
171
|
+
const {table} = build(null);
|
|
172
|
+
expect(column(table, "value").validator).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a table with a bad cell refuses the save, and shows the cell", () => {
|
|
176
|
+
const {form, table} = build(null);
|
|
177
|
+
const $cell = document.createElement("div");
|
|
178
|
+
let shown = 0;
|
|
179
|
+
$cell.scrollIntoView = () => {
|
|
180
|
+
shown++;
|
|
181
|
+
};
|
|
182
|
+
table.answer = [{getElement: () => $cell}]; // what Tabulator answers
|
|
183
|
+
gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
|
|
184
|
+
expect(table.validated).toBe(1);
|
|
185
|
+
expect(saved).toEqual([]);
|
|
186
|
+
expect(shown).toBe(1);
|
|
187
|
+
|
|
188
|
+
table.answer = true;
|
|
189
|
+
gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
|
|
190
|
+
expect(saved.length).toBe(1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -143,6 +143,27 @@ describe("a template field in exec mode (a sub-form)", () => {
|
|
|
143
143
|
expect(errors()).toEqual([]);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test("a colour cleared is black, and its readout says so", () => {
|
|
147
|
+
const form = build({
|
|
148
|
+
template_version: 7,
|
|
149
|
+
alarm_colors: {
|
|
150
|
+
active_not_validated: "#ff0000",
|
|
151
|
+
active_validated: "#e5a50a",
|
|
152
|
+
not_active_not_validated: "#ff00ff"
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const $c = gobj_read_attr(form, "$container");
|
|
156
|
+
const $colors = $c.querySelectorAll("fieldset input");
|
|
157
|
+
expect($colors.length).toBe(3);
|
|
158
|
+
gobj_send_event(form, "EV_CLEAR_RECORD", {}, form);
|
|
159
|
+
for(const $input of $colors) {
|
|
160
|
+
/* Never "": a colour input refuses it (Chrome warns). */
|
|
161
|
+
expect($input.value).toBe("#000000");
|
|
162
|
+
expect($input.yui_color_value.textContent).toBe("#000000");
|
|
163
|
+
}
|
|
164
|
+
expect(errors()).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
146
167
|
test("left null by a save before the fix, comes back with its defaults", () => {
|
|
147
168
|
const form = build({
|
|
148
169
|
template_version: 7,
|