@yuneta/gobj-ui 7.26.36 → 7.26.38

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 CHANGED
@@ -3325,6 +3325,38 @@ keeps working afterwards. With no guard a modal closes exactly as it always
3325
3325
  did, and the returned `close()` always closes **unconditionally** — the veto is
3326
3326
  for the user's dismiss, not for the code's.
3327
3327
 
3328
+ **A guard that ASKS returns the question's promise** (7.26.38+). It is a veto
3329
+ too, and it tells the modal when the question is over: a browser Back that
3330
+ asked gets its history entry back THEN, below nothing. Opening the question and
3331
+ returning `false` put the entry back at once, above the question's own -- so the
3332
+ next Back, the one meant for the question, reached the modal, which asked
3333
+ again, and on a phone (where Back is a gesture) every Back stacked one more
3334
+ question.
3335
+
3336
+ ```js
3337
+ const sheet = yui_shell_show_modal(shell, $body, {
3338
+ dialog: true,
3339
+ title: "device settings",
3340
+ t: t,
3341
+ before_close: () => {
3342
+ const kw = {};
3343
+ gobj_send_event(form, "EV_WINDOW_TO_CLOSE", kw, gobj);
3344
+ if(!kw.abort_close) {
3345
+ return true;
3346
+ }
3347
+ return yui_shell_confirm_yesnocancel(shell, "all changes will be lost", {t})
3348
+ .then((answer) => {
3349
+ if(answer === "yes") {
3350
+ gobj_send_event(gobj, "EV_CLOSE_SHEET", {}, gobj); // its action: sheet.close()
3351
+ }
3352
+ });
3353
+ },
3354
+ on_close: () => {
3355
+ gobj_send_event(gobj, "EV_SHEET_CLOSED", {}, gobj); // its action: destroy the form
3356
+ }
3357
+ });
3358
+ ```
3359
+
3328
3360
  ### Confirmations — and the red one, `yui_shell_confirm_danger`
3329
3361
 
3330
3362
  `yui_shell_confirm_yesno(shell, message, opts)` asks a question and resolves to
@@ -3970,6 +3970,10 @@ function yui_shell_show_modal(shell, content, opts) {
3970
3970
  (0, _yuneta_gobj_js.log_warning)(`yui_shell_show_modal: before_close threw: ${e}`);
3971
3971
  allow = true;
3972
3972
  }
3973
+ if (allow && typeof allow.then === "function") {
3974
+ if (typeof on_veto === "function") allow.then(on_veto, on_veto);
3975
+ return;
3976
+ }
3973
3977
  if (allow === false) {
3974
3978
  if (typeof on_veto === "function") on_veto();
3975
3979
  return;
@@ -3981,7 +3985,7 @@ function yui_shell_show_modal(shell, content, opts) {
3981
3985
  };
3982
3986
  let on_back = function() {
3983
3987
  guarded_close(function() {
3984
- overlay = yui_shell_register_overlay(shell, on_back);
3988
+ if (!closed) overlay = yui_shell_register_overlay(shell, on_back);
3985
3989
  });
3986
3990
  };
3987
3991
  escape_handler = request_close;
@@ -31549,19 +31553,19 @@ function open_form_dialog(gobj, mode, record) {
31549
31553
  }
31550
31554
  /************************************************************
31551
31555
  * Unsaved-changes guard for the dialog's `before_close` hook.
31552
- * Returns true to allow the dismiss; false to VETO it (the form
31553
- * has pending edits) and pop the confirm — which, on "yes",
31554
- * closes the modal itself (EV_WINDOW_TO_CLOSE contract).
31556
+ * Returns true to allow the dismiss; otherwise the form has
31557
+ * pending edits, and it asks: the confirm's promise is a VETO
31558
+ * that tells the dialog when the question is over (a Back that
31559
+ * asked gets its history entry back then, below nothing). On
31560
+ * "yes" the confirm closes the modal itself (EV_WINDOW_TO_CLOSE
31561
+ * contract).
31555
31562
  ************************************************************/
31556
31563
  function form_may_close(gobj) {
31557
31564
  let priv = gobj.priv;
31558
31565
  if (priv.form) {
31559
31566
  let kw = {};
31560
31567
  (0, _yuneta_gobj_js.gobj_send_event)(priv.form, "EV_WINDOW_TO_CLOSE", kw, gobj);
31561
- if (kw.abort_close) {
31562
- confirm_then$1(gobj, kw.warning || "all changes will be lost", { what: "close_form" });
31563
- return false;
31564
- }
31568
+ if (kw.abort_close) return confirm_then$1(gobj, kw.warning || "all changes will be lost", { what: "close_form" });
31565
31569
  }
31566
31570
  return true;
31567
31571
  }
@@ -31595,10 +31599,11 @@ function teardown_form_child(gobj) {
31595
31599
  * it, and a gobj, a Tabulator row or a DOM node in there is a
31596
31600
  * circular structure that breaks the very trace the FSM feeds.
31597
31601
  *
31598
- * Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
31602
+ * Same shape as C_YUI_SCHEMA_EDITOR's confirm_then(). Answers
31603
+ * the promise of the question, settled once it is over.
31599
31604
  ************************************************************/
31600
31605
  function confirm_then$1(gobj, message, kw) {
31601
- yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
31606
+ return yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
31602
31607
  t: i18next.t,
31603
31608
  yes_label: "yes",
31604
31609
  no_label: "no",
@@ -3965,6 +3965,10 @@ function yui_shell_show_modal(shell, content, opts) {
3965
3965
  log_warning(`yui_shell_show_modal: before_close threw: ${e}`);
3966
3966
  allow = true;
3967
3967
  }
3968
+ if (allow && typeof allow.then === "function") {
3969
+ if (typeof on_veto === "function") allow.then(on_veto, on_veto);
3970
+ return;
3971
+ }
3968
3972
  if (allow === false) {
3969
3973
  if (typeof on_veto === "function") on_veto();
3970
3974
  return;
@@ -3976,7 +3980,7 @@ function yui_shell_show_modal(shell, content, opts) {
3976
3980
  };
3977
3981
  let on_back = function() {
3978
3982
  guarded_close(function() {
3979
- overlay = yui_shell_register_overlay(shell, on_back);
3983
+ if (!closed) overlay = yui_shell_register_overlay(shell, on_back);
3980
3984
  });
3981
3985
  };
3982
3986
  escape_handler = request_close;
@@ -31544,19 +31548,19 @@ function open_form_dialog(gobj, mode, record) {
31544
31548
  }
31545
31549
  /************************************************************
31546
31550
  * Unsaved-changes guard for the dialog's `before_close` hook.
31547
- * Returns true to allow the dismiss; false to VETO it (the form
31548
- * has pending edits) and pop the confirm — which, on "yes",
31549
- * closes the modal itself (EV_WINDOW_TO_CLOSE contract).
31551
+ * Returns true to allow the dismiss; otherwise the form has
31552
+ * pending edits, and it asks: the confirm's promise is a VETO
31553
+ * that tells the dialog when the question is over (a Back that
31554
+ * asked gets its history entry back then, below nothing). On
31555
+ * "yes" the confirm closes the modal itself (EV_WINDOW_TO_CLOSE
31556
+ * contract).
31550
31557
  ************************************************************/
31551
31558
  function form_may_close(gobj) {
31552
31559
  let priv = gobj.priv;
31553
31560
  if (priv.form) {
31554
31561
  let kw = {};
31555
31562
  gobj_send_event(priv.form, "EV_WINDOW_TO_CLOSE", kw, gobj);
31556
- if (kw.abort_close) {
31557
- confirm_then$1(gobj, kw.warning || "all changes will be lost", { what: "close_form" });
31558
- return false;
31559
- }
31563
+ if (kw.abort_close) return confirm_then$1(gobj, kw.warning || "all changes will be lost", { what: "close_form" });
31560
31564
  }
31561
31565
  return true;
31562
31566
  }
@@ -31590,10 +31594,11 @@ function teardown_form_child(gobj) {
31590
31594
  * it, and a gobj, a Tabulator row or a DOM node in there is a
31591
31595
  * circular structure that breaks the very trace the FSM feeds.
31592
31596
  *
31593
- * Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
31597
+ * Same shape as C_YUI_SCHEMA_EDITOR's confirm_then(). Answers
31598
+ * the promise of the question, settled once it is over.
31594
31599
  ************************************************************/
31595
31600
  function confirm_then$1(gobj, message, kw) {
31596
- yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
31601
+ return yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
31597
31602
  t,
31598
31603
  yes_label: "yes",
31599
31604
  no_label: "no",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.26.36",
3
+ "version": "7.26.38",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -2554,9 +2554,12 @@ function open_form_dialog(gobj, mode, record)
2554
2554
 
2555
2555
  /************************************************************
2556
2556
  * Unsaved-changes guard for the dialog's `before_close` hook.
2557
- * Returns true to allow the dismiss; false to VETO it (the form
2558
- * has pending edits) and pop the confirm — which, on "yes",
2559
- * closes the modal itself (EV_WINDOW_TO_CLOSE contract).
2557
+ * Returns true to allow the dismiss; otherwise the form has
2558
+ * pending edits, and it asks: the confirm's promise is a VETO
2559
+ * that tells the dialog when the question is over (a Back that
2560
+ * asked gets its history entry back then, below nothing). On
2561
+ * "yes" the confirm closes the modal itself (EV_WINDOW_TO_CLOSE
2562
+ * contract).
2560
2563
  ************************************************************/
2561
2564
  function form_may_close(gobj)
2562
2565
  {
@@ -2570,9 +2573,8 @@ function form_may_close(gobj)
2570
2573
  * a second key to define — and the one that was here had never
2571
2574
  * been defined in any locale, which renders as the English
2572
2575
  * sentence itself, in every language, for ever. */
2573
- confirm_then(gobj, /* i18n: C_YUI_FORM_I18N_KEYS */ kw.warning || "all changes will be lost",
2576
+ return confirm_then(gobj, /* i18n: C_YUI_FORM_I18N_KEYS */ kw.warning || "all changes will be lost",
2574
2577
  {what: "close_form"});
2575
- return false;
2576
2578
  }
2577
2579
  }
2578
2580
  return true;
@@ -2612,11 +2614,12 @@ function teardown_form_child(gobj)
2612
2614
  * it, and a gobj, a Tabulator row or a DOM node in there is a
2613
2615
  * circular structure that breaks the very trace the FSM feeds.
2614
2616
  *
2615
- * Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
2617
+ * Same shape as C_YUI_SCHEMA_EDITOR's confirm_then(). Answers
2618
+ * the promise of the question, settled once it is over.
2616
2619
  ************************************************************/
2617
2620
  function confirm_then(gobj, message, kw)
2618
2621
  {
2619
- yui_shell_confirm_yesnocancel(
2622
+ return yui_shell_confirm_yesnocancel(
2620
2623
  yui_shell_of(gobj), message,
2621
2624
  {t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
2622
2625
  ).then(function(answer) {
@@ -0,0 +1,193 @@
1
+ /***********************************************************************
2
+ * modal_guard_back.wiring.test.js
3
+ *
4
+ * A modal whose `before_close` guard ASKS (unsaved changes). The
5
+ * browser Back takes the modal's history entry before the guard
6
+ * runs; the guard opens a question, and the modal stays. When the
7
+ * modal took its entry back at once, it went ABOVE the question's
8
+ * own entry: the next Back -- the one meant for the question --
9
+ * reached the modal, which asked again, and every Back stacked
10
+ * one more question. A guard that asks answers the question's
11
+ * promise, and the modal takes its entry back once the question
12
+ * is over.
13
+ *
14
+ * Copyright (c) 2026, ArtGins.
15
+ * All Rights Reserved.
16
+ ***********************************************************************/
17
+ import {describe, test, expect, beforeAll, beforeEach} from "vitest";
18
+ import {install_dom_double} from "../test/dom_double.js";
19
+
20
+ install_dom_double();
21
+
22
+ const {
23
+ SDATA_END, gclass_create,
24
+ gobj_start_up, gobj_create_yuno, gobj_create, gobj_start,
25
+ set_log_callback,
26
+ } = await import("@yuneta/gobj-js");
27
+ const {register_c_yui_shell} = await import("./c_yui_shell.js");
28
+ const {
29
+ yui_shell_show_modal,
30
+ yui_shell_confirm_yesnocancel,
31
+ } = await import("./shell_modals.js");
32
+
33
+ const logged = [];
34
+ let yuno = null;
35
+ let seq = 0;
36
+
37
+ beforeAll(async () => {
38
+ const i18next = (await import("i18next")).default;
39
+ await i18next.init({lng: "en", resources: {}});
40
+ gobj_start_up(null, null, null, null, null, null, null);
41
+ set_log_callback((level, msg) => {
42
+ logged.push({level: String(level), msg: String(msg)});
43
+ });
44
+ gclass_create("C_TEST_GUARD_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
45
+ register_c_yui_shell();
46
+ yuno = gobj_create_yuno("guard_yuno", "C_TEST_GUARD_HOST", {});
47
+ gobj_start(yuno);
48
+ });
49
+
50
+ beforeEach(() => {
51
+ logged.length = 0;
52
+ /* What a failed test left open belongs to its own shell. */
53
+ for(const $n of document.querySelectorAll(".CONFIRM, .TEST_SHEET")) {
54
+ if($n.parentNode) {
55
+ $n.parentNode.removeChild($n);
56
+ }
57
+ }
58
+ });
59
+
60
+ function errors()
61
+ {
62
+ return logged.filter((l) => l.level === "error").map((l) => l.msg);
63
+ }
64
+
65
+ /* Same Back as window_veto_back: a Back off a marker lands on the entry
66
+ * under it, which the shell stamped with the TOP overlay's id. */
67
+ function press_back(shell)
68
+ {
69
+ const stack = shell.priv.overlay_stack;
70
+ const ev = new Event("popstate");
71
+ ev.state = {__yui_below__: stack.length ? stack[stack.length - 1].id : 0};
72
+ window.dispatchEvent(ev);
73
+ }
74
+
75
+ function settle()
76
+ {
77
+ return new Promise(resolve => setTimeout(resolve, 0));
78
+ }
79
+
80
+ function sheets()
81
+ {
82
+ return document.querySelectorAll(".TEST_SHEET").length;
83
+ }
84
+
85
+ function questions()
86
+ {
87
+ return document.querySelectorAll(".CONFIRM").length;
88
+ }
89
+
90
+ /* A shell, and a sheet over it whose guard asks while `state.dirty`. */
91
+ function open_guarded_sheet(state)
92
+ {
93
+ seq++;
94
+ const app = gobj_create(`gapp${seq}`, "C_TEST_GUARD_HOST", {}, yuno);
95
+ gobj_start(app);
96
+ const shell = gobj_create(`gshell${seq}`, "C_YUI_SHELL",
97
+ {config: {shell: {}}, default_route: "/home", subscriber: app}, app);
98
+ gobj_start(shell);
99
+ logged.length = 0; /* the shell's own start: no route table here */
100
+
101
+ const handle = yui_shell_show_modal(shell, "the sheet", {
102
+ dialog: true,
103
+ logical_class: "TEST_SHEET",
104
+ title: "settings",
105
+ before_close: function() {
106
+ if(!state.dirty) {
107
+ return true;
108
+ }
109
+ state.asked++;
110
+ return yui_shell_confirm_yesnocancel(shell, "all changes will be lost", {
111
+ yes_label: "yes", no_label: "no", cancel_label: "cancel"
112
+ }).then(function(answer) {
113
+ if(answer === "yes") {
114
+ handle.close();
115
+ }
116
+ });
117
+ }
118
+ });
119
+ return {shell, handle};
120
+ }
121
+
122
+ describe("a modal whose guard asks before it closes", () => {
123
+ test("Back asks ONCE, and the next Back takes the question, not the modal", async () => {
124
+ const state = {dirty: true, asked: 0};
125
+ const {shell, handle} = open_guarded_sheet(state);
126
+ expect(shell.priv.overlay_stack.length).toBe(1);
127
+
128
+ /* Back: the guard asks. The modal stays, and only the question
129
+ * is on the stack -- the modal's entry went with the Back. */
130
+ press_back(shell);
131
+ expect(sheets()).toBe(1);
132
+ expect(questions()).toBe(1);
133
+ expect(state.asked).toBe(1);
134
+ expect(shell.priv.overlay_stack.length).toBe(1);
135
+
136
+ /* Back again: it is the question that goes (its safe default,
137
+ * cancel), and the modal takes its entry back only then. */
138
+ press_back(shell);
139
+ await settle();
140
+ expect(questions()).toBe(0);
141
+ expect(sheets()).toBe(1);
142
+ expect(state.asked).toBe(1);
143
+ expect(shell.priv.overlay_stack.length).toBe(1);
144
+
145
+ /* And a third Back asks again: one question, not two. */
146
+ press_back(shell);
147
+ expect(questions()).toBe(1);
148
+ expect(state.asked).toBe(2);
149
+ press_back(shell);
150
+ await settle();
151
+ expect(questions()).toBe(0);
152
+ expect(sheets()).toBe(1);
153
+ expect(errors()).toEqual([]);
154
+
155
+ handle.close();
156
+ expect(sheets()).toBe(0);
157
+ });
158
+
159
+ test("the back arrow that asks leaves the modal's entry UNDER the question", async () => {
160
+ const state = {dirty: true, asked: 0};
161
+ const {shell, handle} = open_guarded_sheet(state);
162
+ const entry = shell.priv.overlay_stack[0];
163
+
164
+ document.querySelector(".TEST_SHEET .yui-dialog-back").click();
165
+ expect(questions()).toBe(1);
166
+ expect(shell.priv.overlay_stack.length).toBe(2);
167
+ expect(shell.priv.overlay_stack[0]).toBe(entry);
168
+
169
+ press_back(shell);
170
+ await settle();
171
+ expect(questions()).toBe(0);
172
+ expect(sheets()).toBe(1);
173
+ expect(shell.priv.overlay_stack).toEqual([entry]);
174
+ expect(errors()).toEqual([]);
175
+
176
+ handle.close();
177
+ expect(sheets()).toBe(0);
178
+ });
179
+
180
+ test("with nothing to lose, Back closes it at once", async () => {
181
+ const state = {dirty: false, asked: 0};
182
+ const {shell} = open_guarded_sheet(state);
183
+ expect(sheets()).toBe(1);
184
+
185
+ press_back(shell);
186
+ await settle();
187
+ expect(sheets()).toBe(0);
188
+ expect(questions()).toBe(0);
189
+ expect(state.asked).toBe(0);
190
+ expect(shell.priv.overlay_stack).toEqual([]);
191
+ expect(errors()).toEqual([]);
192
+ });
193
+ });
@@ -386,7 +386,17 @@ export function yui_shell_show_modal(shell, content, opts)
386
386
  * VETO — the modal stays up and the caller takes over (e.g. an
387
387
  * unsaved-changes prompt that calls close() itself on confirm). Return
388
388
  * true/undefined to let it close. Absent guard ⇒ always closes, so
389
- * existing callers are unaffected. */
389
+ * existing callers are unaffected.
390
+ *
391
+ * A guard that ASKS first returns the question's PROMISE: a veto, and
392
+ * the modal is told when the question is over. It matters on browser
393
+ * Back, which took the modal's history entry before the guard ran:
394
+ * the entry goes back once the question is gone, never above it.
395
+ * Returned false after opening the question, the entry went back at
396
+ * once, over the question's own, and the next Back -- the one meant
397
+ * for the question -- reached the modal, which asked again: every
398
+ * Back stacked one more question. Same contract as the `asking`
399
+ * close of C_YUI_WINDOW. */
390
400
  let guarded_close = function(on_veto) {
391
401
  if(closed) {
392
402
  return;
@@ -400,6 +410,12 @@ export function yui_shell_show_modal(shell, content, opts)
400
410
  allow = true;
401
411
  }
402
412
  }
413
+ if(allow && typeof allow.then === "function") {
414
+ if(typeof on_veto === "function") {
415
+ allow.then(on_veto, on_veto);
416
+ }
417
+ return;
418
+ }
403
419
  if(allow === false) {
404
420
  if(typeof on_veto === "function") {
405
421
  on_veto();
@@ -412,10 +428,13 @@ export function yui_shell_show_modal(shell, content, opts)
412
428
  guarded_close(null);
413
429
  };
414
430
  /* Browser Back already consumed the history entry; if the guard vetoes,
415
- * re-arm a fresh entry so a later Back still targets this modal. */
431
+ * re-arm a fresh entry so a later Back still targets this modal --
432
+ * unless the answer to its question closed it. */
416
433
  let on_back = function() {
417
434
  guarded_close(function() {
418
- overlay = yui_shell_register_overlay(shell, on_back);
435
+ if(!closed) {
436
+ overlay = yui_shell_register_overlay(shell, on_back);
437
+ }
419
438
  });
420
439
  };
421
440
 
package/src/tabulator.css CHANGED
@@ -186,11 +186,16 @@
186
186
  * (`.tabulator-editing {padding: 0}`) and the control brings its own, so
187
187
  * the text jumps sideways too. Inherit the font and restore the cell's 4px
188
188
  * so entering edit mode changes nothing but the caret.
189
+ *
190
+ * A box to type in, never a box to TICK: a checkbox or a radio given the
191
+ * whole cell is drawn as a square the size of the cell -- the tickCross
192
+ * editor of a boolean (estadodelaire's "validated") opened as a huge empty
193
+ * box on a phone, which read as broken. Those keep their own size.
189
194
  * ==================================================================== */
190
- .tabulator-row .tabulator-cell.tabulator-editing input,
195
+ .tabulator-row .tabulator-cell.tabulator-editing input:not([type=checkbox]):not([type=radio]),
191
196
  .tabulator-row .tabulator-cell.tabulator-editing select,
192
197
  .tabulator-row .tabulator-cell.tabulator-editing textarea,
193
- .tabulator .tabulator-header .tabulator-header-filter input,
198
+ .tabulator .tabulator-header .tabulator-header-filter input:not([type=checkbox]):not([type=radio]),
194
199
  .tabulator .tabulator-header .tabulator-header-filter select {
195
200
  font-family: inherit;
196
201
  font-size: inherit;
@@ -199,11 +204,22 @@
199
204
  padding: 0 4px;
200
205
  }
201
206
 
202
- .tabulator-row .tabulator-cell.tabulator-editing input,
207
+ .tabulator-row .tabulator-cell.tabulator-editing input:not([type=checkbox]):not([type=radio]),
203
208
  .tabulator-row .tabulator-cell.tabulator-editing select {
204
209
  height: 100%;
205
210
  }
206
211
 
212
+ /* The box to tick, sized for a finger where there is one: the browser's
213
+ * own checkbox is ~13px, too small to hit on a phone -- and the cell
214
+ * is no size for it either (above). */
215
+ @media (pointer: coarse) {
216
+ .tabulator-row .tabulator-cell.tabulator-editing input[type=checkbox],
217
+ .tabulator-row .tabulator-cell.tabulator-editing input[type=radio] {
218
+ width: 1.5rem;
219
+ height: 1.5rem;
220
+ }
221
+ }
222
+
207
223
  /*
208
224
  * The CELL EDITOR in dark theme.
209
225
  *
@@ -0,0 +1,63 @@
1
+ /***********************************************************************
2
+ * tabulator_css.test.js
3
+ *
4
+ * The stylesheet of the tables: an editor fills its cell only when
5
+ * it is a box to TYPE in. A checkbox or a radio given width and
6
+ * height 100% is drawn as a square the size of the cell (the
7
+ * tickCross editor of estadodelaire's "validated" opened as a huge
8
+ * empty box on a phone, gobj-ui 7.26.36).
9
+ *
10
+ * Read as text, as the window layer's invariants are: jsdom applies
11
+ * no stylesheet.
12
+ *
13
+ * Copyright (c) 2026, ArtGins.
14
+ * All Rights Reserved.
15
+ ***********************************************************************/
16
+ import {describe, test, expect} from "vitest";
17
+ import {readFileSync} from "node:fs";
18
+ import {dirname, join} from "node:path";
19
+ import {fileURLToPath} from "node:url";
20
+
21
+ const CSS = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "tabulator.css"), "utf8")
22
+ .replace(/\/\*[\s\S]*?\*\//g, "");
23
+
24
+ /* Every rule: its selectors, and its body. */
25
+ function rules()
26
+ {
27
+ const out = [];
28
+ const re = /([^{}]+)\{([^{}]*)\}/g;
29
+ let m;
30
+ while((m = re.exec(CSS)) !== null) {
31
+ out.push({
32
+ selectors: m[1].split(",").map((s) => s.trim()).filter(Boolean),
33
+ body: m[2]
34
+ });
35
+ }
36
+ return out;
37
+ }
38
+
39
+ describe("an editor fills its cell only when it is a box to type in", () => {
40
+ test("no rule sizes a checkbox or a radio of a table to its cell", () => {
41
+ const offenders = [];
42
+ for(const r of rules()) {
43
+ if(!/\b(width|height)\s*:\s*100%/.test(r.body)) {
44
+ continue;
45
+ }
46
+ for(const sel of r.selectors) {
47
+ /* A bare `input` in a cell editor or a header filter reaches
48
+ * a checkbox too, unless the selector leaves it out. */
49
+ if(/\binput\b/.test(sel) && /tabulator/.test(sel) &&
50
+ !(/:not\(\[type=checkbox\]\)/.test(sel) && /:not\(\[type=radio\]\)/.test(sel))) {
51
+ offenders.push(sel);
52
+ }
53
+ }
54
+ }
55
+ expect(offenders).toEqual([]);
56
+ });
57
+
58
+ test("a text input of a cell being edited still fills it", () => {
59
+ const fill = rules().filter((r) => /\bwidth\s*:\s*100%/.test(r.body) &&
60
+ r.selectors.some((s) => /tabulator-editing input:not\(\[type=checkbox\]\)/.test(s)));
61
+ expect(fill.length).toBeGreaterThan(0);
62
+ });
63
+ });