@yuneta/gobj-ui 7.26.39 → 7.26.41

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.
@@ -30,62 +30,12 @@ import {
30
30
  import uPlot from "uplot";
31
31
  import "uplot/dist/uPlot.min.css";
32
32
 
33
- import {yui_is_dark, yui_watch_theme} from "./yui_theme.js";
33
+ import {yui_watch_theme} from "./yui_theme.js";
34
+ /* The ink of the axes -- the X and the Y -- and the palette of the
35
+ * series, on both schemes: see yui_uplot_ink.js. */
36
+ import {yui_uplot_axes, YUI_UPLOT_PALETTE} from "./yui_uplot_ink.js";
34
37
 
35
38
 
36
- /***************************************************************
37
- * The ink of a chart, and its palette.
38
- *
39
- * uPlot is built for a white page: its axis labels are `#000`
40
- * and its grid `rgba(0,0,0,0.07)`, which on the dark scheme are
41
- * 1.16:1 and 1.04 -- an axis nobody can read and a grid that is
42
- * not there. And the series were CSS colour names: `blue` is
43
- * 2.11:1 on the dark ground, `orange` 1.97 on the light one, so
44
- * whichever scheme you were in, one of the lines was a rumour.
45
- *
46
- * These are given to uPlot as FUNCTIONS: it calls them when it
47
- * draws, so the value can never be stale -- what goes stale is
48
- * the canvas, and that is what EV_THEME redraws.
49
- *
50
- * The palette is six colours that clear 3:1 on BOTH grounds
51
- * (worst 3.39 on white, 3.71 on near-black), ordered so the
52
- * first two are the pair that survives colour blindness -- blue
53
- * and orange, never red and green as the opening pair.
54
- ***************************************************************/
55
- function axis_ink()
56
- {
57
- return yui_is_dark()? "#c7cdd8" : "#2e333d";
58
- }
59
-
60
- function grid_ink()
61
- {
62
- return yui_is_dark()? "rgba(199,205,216,0.16)" : "rgba(46,51,61,0.12)";
63
- }
64
-
65
- const CHART_PALETTE = [
66
- {stroke: "#2f6fd0", fill: "rgba(47,111,208,0.10)"}, /* 4.88 / 3.71 */
67
- {stroke: "#b8620a", fill: "rgba(184,98,10,0.10)"}, /* 4.38 / 4.13 */
68
- {stroke: "#2f8f4e", fill: "rgba(47,143,78,0.10)"}, /* 4.07 / 4.45 */
69
- {stroke: "#8b5cf6", fill: "rgba(139,92,246,0.10)"}, /* 4.23 / 4.28 */
70
- {stroke: "#0e9aa7", fill: "rgba(14,154,167,0.10)"}, /* 3.39 / 5.35 */
71
- {stroke: "#d1344b", fill: "rgba(209,52,75,0.10)"}, /* 4.88 / 3.71 */
72
- ];
73
-
74
- /* The axes the host asked for, with the ink it did not ask about. */
75
- function themed_axes(axes)
76
- {
77
- let list = Array.isArray(axes)? axes : [axes || {}];
78
- return list.map((ax) => {
79
- let a = Object.assign({}, ax || {});
80
- if(a.stroke === undefined) {
81
- a.stroke = axis_ink;
82
- }
83
- a.grid = Object.assign({stroke: grid_ink}, a.grid || {});
84
- a.ticks = Object.assign({stroke: grid_ink}, a.ticks || {});
85
- return a;
86
- });
87
- }
88
-
89
39
  /***************************************************************
90
40
  * Constants
91
41
  ***************************************************************/
@@ -104,7 +54,7 @@ SDATA(data_type_t.DTP_JSON, "series", 0, [{}], "Data series"),
104
54
  SDATA(data_type_t.DTP_JSON, "axes", 0, [{}], "Chart axes"),
105
55
  SDATA(data_type_t.DTP_JSON, "scales", 0, {}, "Chart scales"),
106
56
  SDATA(data_type_t.DTP_JSON, "data", 0, [[]], "Chart data"),
107
- SDATA(data_type_t.DTP_JSON, "colors", 0, CHART_PALETTE,
57
+ SDATA(data_type_t.DTP_JSON, "colors", 0, YUI_UPLOT_PALETTE,
108
58
  "Series colours, in order. The default palette clears 3:1 on both schemes and opens with the colour-blind-safe pair (blue, orange)"),
109
59
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTML container for the chart"),
110
60
  SDATA(data_type_t.DTP_BOOLEAN, "own_container", 0, false, "If true, container is gobj-managed"),
@@ -138,7 +88,7 @@ function mt_create(gobj)
138
88
  build_ui(gobj);
139
89
 
140
90
  /* A canvas does not restyle itself: the ink is read when uPlot
141
- * DRAWS (see axis_ink), so a theme change only has to reach the
91
+ * DRAWS (yui_uplot_ink.js), so a theme change only has to reach the
142
92
  * machine and ask for a redraw. Same wiring as the graphs. */
143
93
  gobj.priv.theme_observer = yui_watch_theme(gobj);
144
94
 
@@ -220,7 +170,7 @@ function build_ui(gobj)
220
170
  width: gobj_read_attr(gobj, "width"),
221
171
  height: gobj_read_attr(gobj, "height"),
222
172
  series: gobj_read_attr(gobj, "series"),
223
- axes: themed_axes(gobj_read_attr(gobj, "axes")),
173
+ axes: yui_uplot_axes(gobj_read_attr(gobj, "axes")),
224
174
  scales: gobj_read_attr(gobj, "scales"),
225
175
  };
226
176
  let uplot = new uPlot(
@@ -444,7 +394,7 @@ function ac_add_serie(gobj, event, kw, src)
444
394
  }
445
395
  let colors = gobj_read_attr(gobj, "colors");
446
396
  if(!Array.isArray(colors) || !colors.length) {
447
- colors = CHART_PALETTE;
397
+ colors = YUI_UPLOT_PALETTE;
448
398
  }
449
399
  /* `idx` counts uPlot's series, and series[0] is the X AXIS -- so
450
400
  * the first line drawn was taking colours[1] and the palette's
@@ -0,0 +1,398 @@
1
+ /***********************************************************************
2
+ * form_table_edits_ask.wiring.test.js
3
+ *
4
+ * What the reader does to the TABLES of a C_YUI_FORM is `edited`,
5
+ * whatever the record the form opened with -- so closing asks.
6
+ *
7
+ * A form opens CHANGED when its record lacks a field the template
8
+ * fills with its default. Until 7.26.40 that also unwired the
9
+ * tables' `dataChanged` (and only a save, an undo or a load of what
10
+ * is stored wired it again), so adding a row, editing a cell,
11
+ * deleting a row or editing a nested table marked nothing: since
12
+ * 7.26.39 the close asks only for `edited`, and it closed without a
13
+ * question -- the edit lost. In estadodelaire that is the settings of
14
+ * every device without its alarm colours, whose alarms are tables.
15
+ *
16
+ * And the column titles of those tables change language: they were
17
+ * drawn at build and never again.
18
+ *
19
+ * Driven through the real gclass on a document double, with a
20
+ * Tabulator double that, like Tabulator, draws a row (its formatter
21
+ * runs) and says `dataChanged` INSIDE addRow(), synchronously.
22
+ *
23
+ * Copyright (c) 2026, ArtGins.
24
+ * All Rights Reserved.
25
+ ***********************************************************************/
26
+ import {describe, test, expect, beforeAll, beforeEach, 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.settings = settings;
45
+ this.options = {};
46
+ this.handlers = {};
47
+ this.initialized = true;
48
+ this.rows = [];
49
+ this.installed = {};
50
+ this.locale = null;
51
+ this.modules = {
52
+ localize: {
53
+ installLang: (name, strings) => {
54
+ this.installed[name] = strings;
55
+ }
56
+ }
57
+ };
58
+ tables.push(this);
59
+ return new Proxy(this, {
60
+ get(target, prop) {
61
+ if(prop in target) {
62
+ return target[prop];
63
+ }
64
+ return () => [];
65
+ }
66
+ });
67
+ }
68
+ on(ev, fn) {
69
+ (this.handlers[ev] = this.handlers[ev] || []).push(fn);
70
+ }
71
+ off(ev) {
72
+ delete this.handlers[ev];
73
+ }
74
+ emit(ev) {
75
+ for(const fn of (this.handlers[ev] || [])) {
76
+ fn([]);
77
+ }
78
+ }
79
+ addRow(record) {
80
+ /* The row's element lives in the table's, as in Tabulator:
81
+ * a nested table drawn in it is inside the form. */
82
+ const $row = document.createElement("div");
83
+ this.element.appendChild($row);
84
+ const row = {getElement: () => $row, getData: () => record};
85
+ if(this.settings && typeof this.settings.rowFormatter === "function") {
86
+ this.settings.rowFormatter(row);
87
+ }
88
+ this.rows.push(row);
89
+ this.emit("dataChanged");
90
+ return Promise.resolve(row);
91
+ }
92
+ /* What the trash of a row ends in (row.delete()). */
93
+ delete_row() {
94
+ const row = this.rows.pop();
95
+ row.getElement().remove();
96
+ this.emit("dataChanged");
97
+ }
98
+ /* What an edited cell says. */
99
+ edit_cell() {
100
+ this.emit("dataChanged");
101
+ }
102
+ validate() {
103
+ return true;
104
+ }
105
+ clearData() {
106
+ for(const row of this.rows) {
107
+ row.getElement().remove();
108
+ }
109
+ this.rows = [];
110
+ }
111
+ setLocale(name) {
112
+ this.locale = name;
113
+ }
114
+ destroy() {
115
+ }
116
+ }
117
+ return {TabulatorFull: FakeTabulator, Tabulator: FakeTabulator};
118
+ });
119
+
120
+ const {
121
+ SDATA_END, gclass_create,
122
+ gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
123
+ gobj_start, gobj_send_event, gobj_read_attr,
124
+ set_log_callback,
125
+ } = await import("@yuneta/gobj-js");
126
+ const {register_c_yui_form} = await import("./c_yui_form.js");
127
+ const i18next = (await import("i18next")).default;
128
+
129
+ const logged = [];
130
+ let yuno = null;
131
+
132
+ /* A device's settings, cut down: a defaulted field, and the alarms as
133
+ * a table whose rows carry a nested table of triggers. */
134
+ const TEMPLATE = {
135
+ template_version: {
136
+ type: "integer",
137
+ default: 7,
138
+ flag: ["hidden", "persistent"]
139
+ },
140
+ level: {
141
+ type: "integer",
142
+ default: 3,
143
+ flag: ["writable", "persistent"]
144
+ },
145
+ alarms: {
146
+ type: "array",
147
+ flag: ["writable", "persistent", "table"],
148
+ table: {
149
+ name: {
150
+ type: "string",
151
+ header: "alarm name",
152
+ flag: ["writable", "persistent"]
153
+ },
154
+ triggers: {
155
+ type: "array",
156
+ flag: ["writable", "persistent", "table"],
157
+ table: {
158
+ measure: {
159
+ type: "string",
160
+ header: "measure",
161
+ flag: ["writable", "persistent"]
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ };
168
+
169
+ /* `level` missing: the form opens changed. */
170
+ const LACKING = {
171
+ template_version: 7,
172
+ alarms: [{name: "hot", triggers: [{measure: "temperature"}]}]
173
+ };
174
+
175
+ const COMPLETE = {
176
+ template_version: 7,
177
+ level: 3,
178
+ alarms: [{name: "hot", triggers: [{measure: "temperature"}]}]
179
+ };
180
+
181
+ beforeAll(async () => {
182
+ await i18next.init({
183
+ lng: "en",
184
+ resources: {
185
+ en: {translation: {"alarm name": "Alarm name", "measure": "Measure"}},
186
+ es: {translation: {"alarm name": "Nombre de la alarma", "measure": "Medida"}}
187
+ }
188
+ });
189
+ gobj_start_up(null, null, null, null, null, null, null);
190
+ set_log_callback((level, msg) => {
191
+ logged.push({level: String(level), msg: String(msg)});
192
+ });
193
+ gclass_create("C_TEST_TE_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
194
+ gclass_create(
195
+ "C_TEST_TE_APP",
196
+ [["EV_SAVE_RECORD", 0]],
197
+ [["ST_IDLE", [
198
+ ["EV_SAVE_RECORD", () => 0, null],
199
+ ]]],
200
+ {}, 0, [SDATA_END()], {}, 0, 0, 0, 0
201
+ );
202
+ register_c_yui_form();
203
+ yuno = gobj_create_yuno("form_table_edits_yuno", "C_TEST_TE_HOST", {});
204
+ gobj_start(yuno);
205
+ });
206
+
207
+ beforeEach(async () => {
208
+ logged.length = 0;
209
+ await i18next.changeLanguage("en");
210
+ });
211
+
212
+ let seq = 0;
213
+
214
+ function build(record)
215
+ {
216
+ const first = tables.length;
217
+ const app = gobj_create_pure_child(`teapp${++seq}`, "C_TEST_TE_APP", {}, yuno);
218
+ gobj_start(app);
219
+ const form = gobj_create_pure_child(`teform${seq}`, "C_YUI_FORM", {
220
+ template: TEMPLATE,
221
+ record: record,
222
+ editable: true,
223
+ }, app);
224
+ gobj_start(form);
225
+ return {form, first};
226
+ }
227
+
228
+ /* The tables the form built since `first`: the alarms table first,
229
+ * then a triggers table per alarm row drawn. */
230
+ function built(first)
231
+ {
232
+ return tables.slice(first);
233
+ }
234
+
235
+ /* The microtasks of addRow().then(): the nested rows load there. */
236
+ async function settle()
237
+ {
238
+ await new Promise(resolve => setTimeout(resolve, 0));
239
+ }
240
+
241
+ function close_asks(form)
242
+ {
243
+ const kw = {abort_close: false};
244
+ gobj_send_event(form, "EV_WINDOW_TO_CLOSE", kw, form);
245
+ return kw.abort_close ? (kw.warning || "(no warning)") : "";
246
+ }
247
+
248
+ function errors()
249
+ {
250
+ return logged.filter(l => /error/i.test(l.level) || /ERROR/.test(l.msg));
251
+ }
252
+
253
+ describe("a form that opens changed hears what the reader does to its tables", () => {
254
+ test("it opens changed, not edited, and closes without asking", async () => {
255
+ const {form, first} = build(LACKING);
256
+ await settle();
257
+ expect(gobj_read_attr(form, "changes")).toBe(true);
258
+ expect(gobj_read_attr(form, "edited")).toBe(false);
259
+ expect(built(first).length).toBe(2); // the alarms, and the triggers of its row
260
+ expect(close_asks(form)).toBe("");
261
+ expect(errors()).toEqual([]);
262
+ });
263
+
264
+ test("an edited cell makes it ask", async () => {
265
+ const {form, first} = build(LACKING);
266
+ await settle();
267
+ built(first)[0].edit_cell();
268
+ expect(gobj_read_attr(form, "edited")).toBe(true);
269
+ expect(close_asks(form)).toBe("all changes will be lost");
270
+ expect(errors()).toEqual([]);
271
+ });
272
+
273
+ test("a deleted row makes it ask", async () => {
274
+ const {form, first} = build(LACKING);
275
+ await settle();
276
+ built(first)[0].delete_row();
277
+ expect(close_asks(form)).toBe("all changes will be lost");
278
+ });
279
+
280
+ test("a row the reader adds makes it ask; the rows the form loads do not", async () => {
281
+ const {form, first} = build(LACKING);
282
+ await settle();
283
+ const alarms = built(first)[0];
284
+ expect(alarms.rows.length).toBe(1); // loaded: no question
285
+ expect(close_asks(form)).toBe("");
286
+
287
+ /* What the table's add button sends: the reader's row. */
288
+ const tables_before = tables.length;
289
+ gobj_send_event(form, "EV_ADD_TABLE_ROW", {
290
+ table_id: alarms.element.yui_table_id,
291
+ record: {},
292
+ loading: false
293
+ }, form);
294
+ expect(alarms.rows.length).toBe(2);
295
+ expect(tables.length).toBe(tables_before + 1); // the new row's triggers table
296
+ expect(gobj_read_attr(form, "edited")).toBe(true);
297
+ expect(close_asks(form)).toBe("all changes will be lost");
298
+ expect(errors()).toEqual([]);
299
+ });
300
+
301
+ test("an edit of a NESTED table makes it ask, also one drawn after the load", async () => {
302
+ const loaded = build(LACKING);
303
+ await settle();
304
+ const triggers = built(loaded.first)[1];
305
+ expect(triggers.rows.length).toBe(1); // loaded in its row: no question
306
+ expect(close_asks(loaded.form)).toBe("");
307
+ triggers.edit_cell();
308
+ expect(close_asks(loaded.form)).toBe("all changes will be lost");
309
+
310
+ /* An undo draws the rows again, and each row builds its triggers
311
+ * table then: a table that did not exist when the form loaded. */
312
+ const before_undo = tables.length;
313
+ gobj_send_event(loaded.form, "EV_UNDO_RECORD", {}, loaded.form);
314
+ await settle();
315
+ expect(close_asks(loaded.form)).toBe("");
316
+ const redrawn = tables.slice(before_undo);
317
+ expect(redrawn.length).toBe(1);
318
+ expect(redrawn[0].rows.length).toBe(1);
319
+ redrawn[0].edit_cell();
320
+ expect(close_asks(loaded.form)).toBe("all changes will be lost");
321
+ expect(errors()).toEqual([]);
322
+ });
323
+
324
+ test("a record loaded LATER that lacks a field (a reload, a refused save) still hears them", async () => {
325
+ const {form, first} = build(COMPLETE);
326
+ await settle();
327
+ expect(gobj_read_attr(form, "changes")).toBe(false);
328
+ gobj_send_event(form, "EV_LOAD_RECORD", LACKING, form);
329
+ await settle();
330
+ expect(gobj_read_attr(form, "changes")).toBe(true);
331
+ expect(gobj_read_attr(form, "edited")).toBe(false);
332
+ expect(close_asks(form)).toBe("");
333
+ built(first)[0].edit_cell();
334
+ expect(close_asks(form)).toBe("all changes will be lost");
335
+ expect(errors()).toEqual([]);
336
+ });
337
+
338
+ test("a table built after the load (tableBuilt) takes its rows without a question", async () => {
339
+ const {form, first} = build(COMPLETE);
340
+ await settle();
341
+ const alarms = built(first)[0];
342
+ /* A table not built yet: no rows, and the ones it is given wait
343
+ * for its `tableBuilt`. */
344
+ alarms.clearData();
345
+ alarms.initialized = false;
346
+ gobj_send_event(form, "EV_LOAD_RECORD", LACKING, form);
347
+ await settle();
348
+ expect(alarms.rows.length).toBe(0);
349
+ /* Tabulator builds the table now, long after the load returned. */
350
+ alarms.initialized = true;
351
+ alarms.emit("tableBuilt");
352
+ await settle();
353
+ expect(alarms.rows.length).toBe(1);
354
+ expect(gobj_read_attr(form, "edited")).toBe(false);
355
+ expect(close_asks(form)).toBe("");
356
+ expect(errors()).toEqual([]);
357
+ });
358
+
359
+ test("with a complete record it asks as before", async () => {
360
+ /* The waits are the 200 ms after which 7.26.40 wired the tables
361
+ * again: with them this case passes on the code before too. */
362
+ const wait = () => new Promise(resolve => setTimeout(resolve, 260));
363
+ const {form, first} = build(COMPLETE);
364
+ await wait();
365
+ expect(gobj_read_attr(form, "changes")).toBe(false);
366
+ built(first)[1].edit_cell();
367
+ expect(close_asks(form)).toBe("all changes will be lost");
368
+ gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
369
+ expect(close_asks(form)).toBe("");
370
+ await wait();
371
+ built(first)[0].edit_cell();
372
+ expect(close_asks(form)).toBe("all changes will be lost");
373
+ expect(errors()).toEqual([]);
374
+ });
375
+ });
376
+
377
+ describe("the column titles of the tables change language", () => {
378
+ test("each table, nested ones too, is given its titles in the new language", async () => {
379
+ const {form, first} = build(COMPLETE);
380
+ await settle();
381
+ const [alarms, triggers] = built(first);
382
+
383
+ await i18next.changeLanguage("es");
384
+ gobj_send_event(form, "EV_LANGUAGE_CHANGED", {}, form);
385
+
386
+ expect(alarms.locale).toBeTruthy();
387
+ expect(alarms.installed[alarms.locale].columns).toEqual({
388
+ name: "Nombre de la alarma",
389
+ triggers: "triggers"
390
+ });
391
+ expect(triggers.installed[triggers.locale].columns).toEqual({measure: "Medida"});
392
+ /* No column was rebuilt to get there: the rows (and the nested
393
+ * table) are the ones loaded. */
394
+ expect(alarms.rows.length).toBe(1);
395
+ expect(triggers.rows.length).toBe(1);
396
+ expect(errors()).toEqual([]);
397
+ });
398
+ });
@@ -5,11 +5,13 @@
5
5
  * a save or an undo -- which is what a dialog does: it closes the
6
6
  * moment the record is saved.
7
7
  *
8
- * set_changed_stated(false) re-wires the tables' `dataChanged` from
9
- * a 200 ms timer. Since 7.26.7 destroy_ui() destroys the widgets and
10
- * nulls `$table.tabulator`, and the timer, left running, called
11
- * `.on()` on that null: an uncaught TypeError. The timer is kept and
12
- * cancelled now, and it skips a table that is gone.
8
+ * set_changed_stated(false) used to re-wire the tables' `dataChanged`
9
+ * from a 200 ms timer. Since 7.26.7 destroy_ui() destroys the widgets
10
+ * and nulls `$table.tabulator`, and the timer, left running, called
11
+ * `.on()` on that null: an uncaught TypeError. Since 7.26.41 there is
12
+ * no timer: each table is wired once, when it is built, and stays
13
+ * wired (form_table_edits_ask.wiring.test.js says why). These cases
14
+ * keep guarding the destroy right after a save or an undo.
13
15
  *
14
16
  * Copyright (c) 2026, ArtGins.
15
17
  * All Rights Reserved.
@@ -117,7 +119,7 @@ function build(rows)
117
119
  }
118
120
 
119
121
  describe("a form with a table, destroyed right after", () => {
120
- test("an undo: the deferred re-wiring does not throw", async () => {
122
+ test("an undo, then the destroy: nothing left behind throws", async () => {
121
123
  const {form} = build();
122
124
  expect(tables.length).toBeGreaterThan(0);
123
125
  const errors = [];
@@ -135,7 +137,7 @@ describe("a form with a table, destroyed right after", () => {
135
137
  expect(errors).toEqual([]);
136
138
  });
137
139
 
138
- test("a live form is still re-wired", async () => {
140
+ test("a live form's table stays wired, once", async () => {
139
141
  const {form} = build();
140
142
  const table = tables[tables.length - 1];
141
143
  gobj_send_event(form, "EV_UNDO_RECORD", {}, form);
@@ -0,0 +1,105 @@
1
+ /***********************************************************************
2
+ * uplot_ink.test.js
3
+ *
4
+ * The axes of a chart carry the ink of the theme -- the Y axis
5
+ * too. uPlot always draws one (its setDefaults() makes axes[1]
6
+ * whether it was given or not, with its own black), and
7
+ * C_YUI_UPLOT inked only the axes it was handed: by default one,
8
+ * the X. In the dark scheme the values of every Y axis of the
9
+ * library were black on near-black (a user's report, the cards of
10
+ * estadodelaire).
11
+ *
12
+ * Copyright (c) 2026, ArtGins.
13
+ * All Rights Reserved.
14
+ ***********************************************************************/
15
+ import {describe, test, expect, beforeAll, beforeEach, afterEach, vi} from "vitest";
16
+ import {install_dom_double} from "../test/dom_double.js";
17
+
18
+ install_dom_double();
19
+
20
+ /* What C_YUI_UPLOT hands to uPlot, kept by the stand-in. */
21
+ let given = null;
22
+ vi.mock("uplot", () => ({
23
+ default: class {
24
+ constructor(opts) {
25
+ given = opts;
26
+ this.series = [{}];
27
+ }
28
+ setSize() {}
29
+ addSeries() {}
30
+ redraw() {}
31
+ destroy() {}
32
+ }
33
+ }));
34
+
35
+ const {
36
+ gobj_start_up, gobj_create_yuno, gobj_create_pure_child, gclass_create,
37
+ set_log_callback, SDATA_END,
38
+ } = await import("@yuneta/gobj-js");
39
+ const {register_c_yui_uplot} = await import("./c_yui_uplot.js");
40
+ const {
41
+ yui_uplot_axes, yui_uplot_axis_ink, yui_uplot_grid_ink,
42
+ } = await import("./yui_uplot_ink.js");
43
+
44
+ const logged = [];
45
+ let yuno = null;
46
+
47
+ beforeAll(() => {
48
+ gobj_start_up(null, null, null, null, null, null, null);
49
+ set_log_callback((level, msg) => {
50
+ logged.push({level: String(level), msg: String(msg)});
51
+ });
52
+ gclass_create("C_TEST_INK_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
53
+ register_c_yui_uplot();
54
+ yuno = gobj_create_yuno("ink_yuno", "C_TEST_INK_HOST", {});
55
+ });
56
+
57
+ beforeEach(() => {
58
+ logged.length = 0;
59
+ given = null;
60
+ });
61
+
62
+ afterEach(() => {
63
+ document.documentElement.removeAttribute("data-theme");
64
+ });
65
+
66
+ describe("yui_uplot_axes()", () => {
67
+ test("gives the X and the Y axes, the two uPlot draws", () => {
68
+ expect(yui_uplot_axes([{}]).length).toBe(2);
69
+ expect(yui_uplot_axes(undefined).length).toBe(2);
70
+ expect(yui_uplot_axes([]).length).toBe(2);
71
+ expect(yui_uplot_axes([{}, {}, {scale: "%"}]).length).toBe(3);
72
+ });
73
+
74
+ test("inks each one with the theme, read when uPlot draws", () => {
75
+ const axes = yui_uplot_axes([{}]);
76
+ for(const a of axes) {
77
+ expect(a.stroke).toBe(yui_uplot_axis_ink);
78
+ expect(a.grid.stroke).toBe(yui_uplot_grid_ink);
79
+ expect(a.ticks.stroke).toBe(yui_uplot_grid_ink);
80
+ }
81
+ document.documentElement.setAttribute("data-theme", "light");
82
+ expect(axes[1].stroke()).toBe("#2e333d");
83
+ document.documentElement.setAttribute("data-theme", "dark");
84
+ expect(axes[1].stroke()).toBe("#c7cdd8");
85
+ });
86
+
87
+ test("keeps what the host gave, and does not touch its objects", () => {
88
+ const mine = [{stroke: "red", grid: {show: false}}];
89
+ const axes = yui_uplot_axes(mine);
90
+ expect(axes[0].stroke).toBe("red");
91
+ expect(axes[0].grid).toEqual({stroke: yui_uplot_grid_ink, show: false});
92
+ expect(mine).toEqual([{stroke: "red", grid: {show: false}}]);
93
+ });
94
+ });
95
+
96
+ describe("C_YUI_UPLOT", () => {
97
+ test("hands uPlot a Y axis with the ink of the theme", () => {
98
+ gobj_create_pure_child("ink_chart", "C_YUI_UPLOT", {}, yuno);
99
+ expect(given).toBeTruthy();
100
+ expect(given.axes.length).toBe(2);
101
+ expect(given.axes[1].stroke).toBe(yui_uplot_axis_ink);
102
+ expect(given.axes[1].grid.stroke).toBe(yui_uplot_grid_ink);
103
+ expect(logged.filter((l) => /error/i.test(l.level))).toEqual([]);
104
+ });
105
+ });
@@ -14,6 +14,12 @@
14
14
  * new Tabulator($el, {..., ...yui_tabulator_lang(t)});
15
15
  * yui_tabulator_relocalize(table, t); // on a language change
16
16
  *
17
+ * A view whose COLUMN TITLES change language hands them too, in the
18
+ * new language, by field -- Tabulator binds each column's title to its
19
+ * locale, so they change with it and no column is rebuilt:
20
+ *
21
+ * yui_tabulator_relocalize(table, t, {device_id: t("device id")});
22
+ *
17
23
  * Every key carries an English `defaultValue`, so an app that does not
18
24
  * define it renders exactly what Tabulator rendered before — the
19
25
  * keys are an OPPORTUNITY to translate, never a requirement.
@@ -236,7 +242,18 @@ function yui_tabulator_name_editor(column_def, name_of)
236
242
  };
237
243
  }
238
244
 
239
- function yui_tabulator_relocalize(table, t)
245
+ /***************************************************************
246
+ * yui_tabulator_relocalize() -- see "Put an EXISTING table in the
247
+ * current language" above.
248
+ *
249
+ * `titles`, optional: {field: title}, the titles of the columns in
250
+ * the new language, already translated. They go in the locale
251
+ * (`columns`), where Tabulator binds each column's title: setLocale()
252
+ * draws them again without rebuilding a column -- updateDefinition()
253
+ * re-renders every row, and a row whose formatter builds something (a
254
+ * nested table) builds it anew. A field left out keeps its title.
255
+ ***************************************************************/
256
+ function yui_tabulator_relocalize(table, t, titles)
240
257
  {
241
258
  if(!table) {
242
259
  return;
@@ -244,6 +261,9 @@ function yui_tabulator_relocalize(table, t)
244
261
  try {
245
262
  let name = next_lang_name();
246
263
  let strings = tabulator_strings(t);
264
+ if(titles && typeof titles === "object") {
265
+ strings.columns = Object.assign({}, titles);
266
+ }
247
267
 
248
268
  /* Tabulator DEEP-CLONES options.langs into its localize module when
249
269
  * the table is built, and never looks at the option again: writing a