@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.
@@ -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 (!$table.tabulator.validate()) validated = false;
10643
+ if (!table_valid($table.tabulator)) validated = false;
10636
10644
  $table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
10637
- if (!$table_nested.tabulator.validate()) validated = false;
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 === "select-multiple") $input.tom_select.clear();
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();
@@ -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 (!$table.tabulator.validate()) validated = false;
10638
+ if (!table_valid($table.tabulator)) validated = false;
10631
10639
  $table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
10632
- if (!$table_nested.tabulator.validate()) validated = false;
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 === "select-multiple") $input.tom_select.clear();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.26.33",
3
+ "version": "7.26.34",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
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(!$table.tabulator.validate()) {
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(!$table_nested.tabulator.validate()) {
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,