@yuneta/gobj-ui 7.26.37 → 7.26.39
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 +52 -0
- package/dist/gobj-ui.cjs.js +39 -13
- package/dist/gobj-ui.es.js +39 -13
- package/package.json +1 -1
- package/src/c_yui_form.js +26 -3
- package/src/c_yui_treedb_topic_with_form.js +10 -7
- package/src/form_close_asks_for_edits.wiring.test.js +197 -0
- package/src/modal_guard_back.wiring.test.js +193 -0
- package/src/shell_modals.js +22 -3
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
|
|
@@ -3362,6 +3394,26 @@ the middle, and **a toolbar left with a single group is centred**. An unknown
|
|
|
3362
3394
|
name is reported, not silently dropped: a typo would otherwise remove the save
|
|
3363
3395
|
button with no trace of why.
|
|
3364
3396
|
|
|
3397
|
+
### `C_YUI_FORM` — what closing it asks about
|
|
3398
|
+
|
|
3399
|
+
`changes` says the form is not what is stored: the reader changed it, or the
|
|
3400
|
+
record lacks a field that has a default, and the form shows the default. The
|
|
3401
|
+
save lights with it. **`edited`** (7.26.39+) says the READER changed it --
|
|
3402
|
+
typed, cleared, pasted -- since it was loaded, saved or undone, and it is the
|
|
3403
|
+
only thing a close asks about: the form answers `EV_WINDOW_TO_CLOSE` with
|
|
3404
|
+
`abort_close` and the warning *"all changes will be lost"* when `edited` is
|
|
3405
|
+
true. A record that lacks a defaulted field opens changed and closes without
|
|
3406
|
+
a question; until 7.26.39 it asked on every close, about nothing the reader
|
|
3407
|
+
would lose.
|
|
3408
|
+
|
|
3409
|
+
```js
|
|
3410
|
+
const kw = {abort_close: false};
|
|
3411
|
+
gobj_send_event(form, "EV_WINDOW_TO_CLOSE", kw, gobj);
|
|
3412
|
+
if(kw.abort_close) {
|
|
3413
|
+
// the reader changed it: ask kw.warning before closing
|
|
3414
|
+
}
|
|
3415
|
+
```
|
|
3416
|
+
|
|
3365
3417
|
### `C_YUI_FORM` — which field is editable, and the one `writable` does not govern
|
|
3366
3418
|
|
|
3367
3419
|
A form opened to LOOK at a record has no editable field: that is a property of
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -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;
|
|
@@ -9479,6 +9483,7 @@ var attrs_table$17 = [
|
|
|
9479
9483
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "form_id", 0, "", "Form identifier"),
|
|
9480
9484
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "initialized", 0, false, "Indicates if it is initialized"),
|
|
9481
9485
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "changes", 0, false, "Indicates if changes have been made"),
|
|
9486
|
+
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "edited", 0, false, "The reader changed the form (typed, cleared, pasted) since it was loaded, saved or undone: what closing it would lose. A field the record lacks, filled with its default, is a change of the form (`changes`) and not of the reader"),
|
|
9482
9487
|
(0, _yuneta_gobj_js.SDATA_END)()
|
|
9483
9488
|
];
|
|
9484
9489
|
var PRIVATE_DATA$17 = {
|
|
@@ -9572,6 +9577,7 @@ function set_changed_stated(gobj, changed) {
|
|
|
9572
9577
|
});
|
|
9573
9578
|
} else {
|
|
9574
9579
|
(0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "changes", false);
|
|
9580
|
+
(0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "edited", false);
|
|
9575
9581
|
enable($button_save, false, OFF_COLOR);
|
|
9576
9582
|
enable($button_undo, false, OFF_COLOR);
|
|
9577
9583
|
if (gobj.priv.rearm_timer) clearTimeout(gobj.priv.rearm_timer);
|
|
@@ -9595,6 +9601,16 @@ function set_changed_stated(gobj, changed) {
|
|
|
9595
9601
|
}
|
|
9596
9602
|
}
|
|
9597
9603
|
/************************************************************
|
|
9604
|
+
* A change the READER made -- typed, cleared, pasted: what
|
|
9605
|
+
* closing the form would lose (ac_window_to_close). Clearing
|
|
9606
|
+
* and pasting wrote their values and marked nothing, so a form
|
|
9607
|
+
* cleared or pasted into kept its save off.
|
|
9608
|
+
************************************************************/
|
|
9609
|
+
function set_edited_by_reader(gobj) {
|
|
9610
|
+
set_changed_stated(gobj, true);
|
|
9611
|
+
(0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "edited", true);
|
|
9612
|
+
}
|
|
9613
|
+
/************************************************************
|
|
9598
9614
|
*
|
|
9599
9615
|
************************************************************/
|
|
9600
9616
|
async function readClipboard$1(gobj) {
|
|
@@ -11976,10 +11992,16 @@ function form_value_2_treedb_value(gobj, field_desc, value) {
|
|
|
11976
11992
|
* Actions
|
|
11977
11993
|
***************************/
|
|
11978
11994
|
/************************************************************
|
|
11979
|
-
* From Window parent, is going to close
|
|
11995
|
+
* From Window parent, is going to close.
|
|
11996
|
+
*
|
|
11997
|
+
* It asks only for what the READER changed (`edited`). A record
|
|
11998
|
+
* that lacks a field opens CHANGED -- the default fills it, and
|
|
11999
|
+
* the save lights to store it -- but the reader did nothing, and
|
|
12000
|
+
* the question asked of every such record, on every close, was
|
|
12001
|
+
* about nothing they would lose.
|
|
11980
12002
|
************************************************************/
|
|
11981
12003
|
function ac_window_to_close(gobj, event, kw, src) {
|
|
11982
|
-
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "
|
|
12004
|
+
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "edited")) {
|
|
11983
12005
|
kw.abort_close = true;
|
|
11984
12006
|
kw.warning = "all changes will be lost";
|
|
11985
12007
|
}
|
|
@@ -11995,6 +12017,7 @@ function ac_load_record(gobj, event, kw, src) {
|
|
|
11995
12017
|
let modified = set_form_values(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, kw);
|
|
11996
12018
|
apply_form_mode(gobj, $form);
|
|
11997
12019
|
set_changed_stated(gobj, modified);
|
|
12020
|
+
(0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "edited", false);
|
|
11998
12021
|
(0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", true);
|
|
11999
12022
|
return 0;
|
|
12000
12023
|
}
|
|
@@ -12038,6 +12061,7 @@ function ac_clear_record(gobj, event, kw, src) {
|
|
|
12038
12061
|
let $form = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container").querySelector("form");
|
|
12039
12062
|
clear_validation(gobj, $form);
|
|
12040
12063
|
clear_data(gobj, $form);
|
|
12064
|
+
set_edited_by_reader(gobj);
|
|
12041
12065
|
return 0;
|
|
12042
12066
|
}
|
|
12043
12067
|
/************************************************************
|
|
@@ -12116,7 +12140,7 @@ function ac_add_table_row(gobj, event, kw, src) {
|
|
|
12116
12140
|
* Internal event
|
|
12117
12141
|
************************************************************/
|
|
12118
12142
|
function ac_record_changed(gobj, event, kw, src) {
|
|
12119
|
-
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "initialized"))
|
|
12143
|
+
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "initialized")) set_edited_by_reader(gobj);
|
|
12120
12144
|
return 0;
|
|
12121
12145
|
}
|
|
12122
12146
|
/************************************************************
|
|
@@ -12139,6 +12163,7 @@ function ac_paste_record(gobj, event, kw, src) {
|
|
|
12139
12163
|
if ((0, _yuneta_gobj_js.is_object)(record)) {
|
|
12140
12164
|
(0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", true);
|
|
12141
12165
|
set_form_values(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, record);
|
|
12166
|
+
set_edited_by_reader(gobj);
|
|
12142
12167
|
}
|
|
12143
12168
|
return 0;
|
|
12144
12169
|
}
|
|
@@ -31549,19 +31574,19 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
31549
31574
|
}
|
|
31550
31575
|
/************************************************************
|
|
31551
31576
|
* Unsaved-changes guard for the dialog's `before_close` hook.
|
|
31552
|
-
* Returns true to allow the dismiss;
|
|
31553
|
-
*
|
|
31554
|
-
*
|
|
31577
|
+
* Returns true to allow the dismiss; otherwise the form has
|
|
31578
|
+
* pending edits, and it asks: the confirm's promise is a VETO
|
|
31579
|
+
* that tells the dialog when the question is over (a Back that
|
|
31580
|
+
* asked gets its history entry back then, below nothing). On
|
|
31581
|
+
* "yes" the confirm closes the modal itself (EV_WINDOW_TO_CLOSE
|
|
31582
|
+
* contract).
|
|
31555
31583
|
************************************************************/
|
|
31556
31584
|
function form_may_close(gobj) {
|
|
31557
31585
|
let priv = gobj.priv;
|
|
31558
31586
|
if (priv.form) {
|
|
31559
31587
|
let kw = {};
|
|
31560
31588
|
(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
|
-
}
|
|
31589
|
+
if (kw.abort_close) return confirm_then$1(gobj, kw.warning || "all changes will be lost", { what: "close_form" });
|
|
31565
31590
|
}
|
|
31566
31591
|
return true;
|
|
31567
31592
|
}
|
|
@@ -31595,10 +31620,11 @@ function teardown_form_child(gobj) {
|
|
|
31595
31620
|
* it, and a gobj, a Tabulator row or a DOM node in there is a
|
|
31596
31621
|
* circular structure that breaks the very trace the FSM feeds.
|
|
31597
31622
|
*
|
|
31598
|
-
* Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
|
|
31623
|
+
* Same shape as C_YUI_SCHEMA_EDITOR's confirm_then(). Answers
|
|
31624
|
+
* the promise of the question, settled once it is over.
|
|
31599
31625
|
************************************************************/
|
|
31600
31626
|
function confirm_then$1(gobj, message, kw) {
|
|
31601
|
-
yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
|
|
31627
|
+
return yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
|
|
31602
31628
|
t: i18next.t,
|
|
31603
31629
|
yes_label: "yes",
|
|
31604
31630
|
no_label: "no",
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -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;
|
|
@@ -9474,6 +9478,7 @@ var attrs_table$17 = [
|
|
|
9474
9478
|
SDATA(data_type_t.DTP_STRING, "form_id", 0, "", "Form identifier"),
|
|
9475
9479
|
SDATA(data_type_t.DTP_BOOLEAN, "initialized", 0, false, "Indicates if it is initialized"),
|
|
9476
9480
|
SDATA(data_type_t.DTP_BOOLEAN, "changes", 0, false, "Indicates if changes have been made"),
|
|
9481
|
+
SDATA(data_type_t.DTP_BOOLEAN, "edited", 0, false, "The reader changed the form (typed, cleared, pasted) since it was loaded, saved or undone: what closing it would lose. A field the record lacks, filled with its default, is a change of the form (`changes`) and not of the reader"),
|
|
9477
9482
|
SDATA_END()
|
|
9478
9483
|
];
|
|
9479
9484
|
var PRIVATE_DATA$17 = {
|
|
@@ -9567,6 +9572,7 @@ function set_changed_stated(gobj, changed) {
|
|
|
9567
9572
|
});
|
|
9568
9573
|
} else {
|
|
9569
9574
|
gobj_write_bool_attr(gobj, "changes", false);
|
|
9575
|
+
gobj_write_bool_attr(gobj, "edited", false);
|
|
9570
9576
|
enable($button_save, false, OFF_COLOR);
|
|
9571
9577
|
enable($button_undo, false, OFF_COLOR);
|
|
9572
9578
|
if (gobj.priv.rearm_timer) clearTimeout(gobj.priv.rearm_timer);
|
|
@@ -9590,6 +9596,16 @@ function set_changed_stated(gobj, changed) {
|
|
|
9590
9596
|
}
|
|
9591
9597
|
}
|
|
9592
9598
|
/************************************************************
|
|
9599
|
+
* A change the READER made -- typed, cleared, pasted: what
|
|
9600
|
+
* closing the form would lose (ac_window_to_close). Clearing
|
|
9601
|
+
* and pasting wrote their values and marked nothing, so a form
|
|
9602
|
+
* cleared or pasted into kept its save off.
|
|
9603
|
+
************************************************************/
|
|
9604
|
+
function set_edited_by_reader(gobj) {
|
|
9605
|
+
set_changed_stated(gobj, true);
|
|
9606
|
+
gobj_write_bool_attr(gobj, "edited", true);
|
|
9607
|
+
}
|
|
9608
|
+
/************************************************************
|
|
9593
9609
|
*
|
|
9594
9610
|
************************************************************/
|
|
9595
9611
|
async function readClipboard$1(gobj) {
|
|
@@ -11971,10 +11987,16 @@ function form_value_2_treedb_value(gobj, field_desc, value) {
|
|
|
11971
11987
|
* Actions
|
|
11972
11988
|
***************************/
|
|
11973
11989
|
/************************************************************
|
|
11974
|
-
* From Window parent, is going to close
|
|
11990
|
+
* From Window parent, is going to close.
|
|
11991
|
+
*
|
|
11992
|
+
* It asks only for what the READER changed (`edited`). A record
|
|
11993
|
+
* that lacks a field opens CHANGED -- the default fills it, and
|
|
11994
|
+
* the save lights to store it -- but the reader did nothing, and
|
|
11995
|
+
* the question asked of every such record, on every close, was
|
|
11996
|
+
* about nothing they would lose.
|
|
11975
11997
|
************************************************************/
|
|
11976
11998
|
function ac_window_to_close(gobj, event, kw, src) {
|
|
11977
|
-
if (gobj_read_bool_attr(gobj, "
|
|
11999
|
+
if (gobj_read_bool_attr(gobj, "edited")) {
|
|
11978
12000
|
kw.abort_close = true;
|
|
11979
12001
|
kw.warning = "all changes will be lost";
|
|
11980
12002
|
}
|
|
@@ -11990,6 +12012,7 @@ function ac_load_record(gobj, event, kw, src) {
|
|
|
11990
12012
|
let modified = set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, kw);
|
|
11991
12013
|
apply_form_mode(gobj, $form);
|
|
11992
12014
|
set_changed_stated(gobj, modified);
|
|
12015
|
+
gobj_write_bool_attr(gobj, "edited", false);
|
|
11993
12016
|
gobj_write_bool_attr(gobj, "initialized", true);
|
|
11994
12017
|
return 0;
|
|
11995
12018
|
}
|
|
@@ -12033,6 +12056,7 @@ function ac_clear_record(gobj, event, kw, src) {
|
|
|
12033
12056
|
let $form = gobj_read_attr(gobj, "$container").querySelector("form");
|
|
12034
12057
|
clear_validation(gobj, $form);
|
|
12035
12058
|
clear_data(gobj, $form);
|
|
12059
|
+
set_edited_by_reader(gobj);
|
|
12036
12060
|
return 0;
|
|
12037
12061
|
}
|
|
12038
12062
|
/************************************************************
|
|
@@ -12111,7 +12135,7 @@ function ac_add_table_row(gobj, event, kw, src) {
|
|
|
12111
12135
|
* Internal event
|
|
12112
12136
|
************************************************************/
|
|
12113
12137
|
function ac_record_changed(gobj, event, kw, src) {
|
|
12114
|
-
if (gobj_read_bool_attr(gobj, "initialized"))
|
|
12138
|
+
if (gobj_read_bool_attr(gobj, "initialized")) set_edited_by_reader(gobj);
|
|
12115
12139
|
return 0;
|
|
12116
12140
|
}
|
|
12117
12141
|
/************************************************************
|
|
@@ -12134,6 +12158,7 @@ function ac_paste_record(gobj, event, kw, src) {
|
|
|
12134
12158
|
if (is_object(record)) {
|
|
12135
12159
|
gobj_write_bool_attr(gobj, "initialized", true);
|
|
12136
12160
|
set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, record);
|
|
12161
|
+
set_edited_by_reader(gobj);
|
|
12137
12162
|
}
|
|
12138
12163
|
return 0;
|
|
12139
12164
|
}
|
|
@@ -31544,19 +31569,19 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
31544
31569
|
}
|
|
31545
31570
|
/************************************************************
|
|
31546
31571
|
* Unsaved-changes guard for the dialog's `before_close` hook.
|
|
31547
|
-
* Returns true to allow the dismiss;
|
|
31548
|
-
*
|
|
31549
|
-
*
|
|
31572
|
+
* Returns true to allow the dismiss; otherwise the form has
|
|
31573
|
+
* pending edits, and it asks: the confirm's promise is a VETO
|
|
31574
|
+
* that tells the dialog when the question is over (a Back that
|
|
31575
|
+
* asked gets its history entry back then, below nothing). On
|
|
31576
|
+
* "yes" the confirm closes the modal itself (EV_WINDOW_TO_CLOSE
|
|
31577
|
+
* contract).
|
|
31550
31578
|
************************************************************/
|
|
31551
31579
|
function form_may_close(gobj) {
|
|
31552
31580
|
let priv = gobj.priv;
|
|
31553
31581
|
if (priv.form) {
|
|
31554
31582
|
let kw = {};
|
|
31555
31583
|
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
|
-
}
|
|
31584
|
+
if (kw.abort_close) return confirm_then$1(gobj, kw.warning || "all changes will be lost", { what: "close_form" });
|
|
31560
31585
|
}
|
|
31561
31586
|
return true;
|
|
31562
31587
|
}
|
|
@@ -31590,10 +31615,11 @@ function teardown_form_child(gobj) {
|
|
|
31590
31615
|
* it, and a gobj, a Tabulator row or a DOM node in there is a
|
|
31591
31616
|
* circular structure that breaks the very trace the FSM feeds.
|
|
31592
31617
|
*
|
|
31593
|
-
* Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
|
|
31618
|
+
* Same shape as C_YUI_SCHEMA_EDITOR's confirm_then(). Answers
|
|
31619
|
+
* the promise of the question, settled once it is over.
|
|
31594
31620
|
************************************************************/
|
|
31595
31621
|
function confirm_then$1(gobj, message, kw) {
|
|
31596
|
-
yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
|
|
31622
|
+
return yui_shell_confirm_yesnocancel(yui_shell_of(gobj), message, {
|
|
31597
31623
|
t,
|
|
31598
31624
|
yes_label: "yes",
|
|
31599
31625
|
no_label: "no",
|
package/package.json
CHANGED
package/src/c_yui_form.js
CHANGED
|
@@ -167,6 +167,7 @@ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Container eleme
|
|
|
167
167
|
SDATA(data_type_t.DTP_STRING, "form_id", 0, "", "Form identifier"),
|
|
168
168
|
SDATA(data_type_t.DTP_BOOLEAN, "initialized", 0, false, "Indicates if it is initialized"),
|
|
169
169
|
SDATA(data_type_t.DTP_BOOLEAN, "changes", 0, false, "Indicates if changes have been made"),
|
|
170
|
+
SDATA(data_type_t.DTP_BOOLEAN, "edited", 0, false, "The reader changed the form (typed, cleared, pasted) since it was loaded, saved or undone: what closing it would lose. A field the record lacks, filled with its default, is a change of the form (`changes`) and not of the reader"),
|
|
170
171
|
SDATA_END()
|
|
171
172
|
];
|
|
172
173
|
|
|
@@ -349,6 +350,7 @@ function set_changed_stated(gobj, changed)
|
|
|
349
350
|
* Data not changed, disable buttons to save/undo
|
|
350
351
|
*/
|
|
351
352
|
gobj_write_bool_attr(gobj, "changes", false);
|
|
353
|
+
gobj_write_bool_attr(gobj, "edited", false);
|
|
352
354
|
|
|
353
355
|
enable($button_save, false, OFF_COLOR);
|
|
354
356
|
enable($button_undo, false, OFF_COLOR);
|
|
@@ -390,6 +392,18 @@ function set_changed_stated(gobj, changed)
|
|
|
390
392
|
}
|
|
391
393
|
}
|
|
392
394
|
|
|
395
|
+
/************************************************************
|
|
396
|
+
* A change the READER made -- typed, cleared, pasted: what
|
|
397
|
+
* closing the form would lose (ac_window_to_close). Clearing
|
|
398
|
+
* and pasting wrote their values and marked nothing, so a form
|
|
399
|
+
* cleared or pasted into kept its save off.
|
|
400
|
+
************************************************************/
|
|
401
|
+
function set_edited_by_reader(gobj)
|
|
402
|
+
{
|
|
403
|
+
set_changed_stated(gobj, true);
|
|
404
|
+
gobj_write_bool_attr(gobj, "edited", true);
|
|
405
|
+
}
|
|
406
|
+
|
|
393
407
|
/************************************************************
|
|
394
408
|
*
|
|
395
409
|
************************************************************/
|
|
@@ -3606,11 +3620,17 @@ function form_value_2_treedb_value(gobj, field_desc, value)
|
|
|
3606
3620
|
|
|
3607
3621
|
|
|
3608
3622
|
/************************************************************
|
|
3609
|
-
* From Window parent, is going to close
|
|
3623
|
+
* From Window parent, is going to close.
|
|
3624
|
+
*
|
|
3625
|
+
* It asks only for what the READER changed (`edited`). A record
|
|
3626
|
+
* that lacks a field opens CHANGED -- the default fills it, and
|
|
3627
|
+
* the save lights to store it -- but the reader did nothing, and
|
|
3628
|
+
* the question asked of every such record, on every close, was
|
|
3629
|
+
* about nothing they would lose.
|
|
3610
3630
|
************************************************************/
|
|
3611
3631
|
function ac_window_to_close(gobj, event, kw, src)
|
|
3612
3632
|
{
|
|
3613
|
-
if(gobj_read_bool_attr(gobj, "
|
|
3633
|
+
if(gobj_read_bool_attr(gobj, "edited")) {
|
|
3614
3634
|
kw.abort_close = true; // Don't close the window until fields repaired
|
|
3615
3635
|
/* An i18n KEY, not a sentence: the dialog renders its message with
|
|
3616
3636
|
* `i18n: message`. The English sentence that was here was a key
|
|
@@ -3634,6 +3654,7 @@ function ac_load_record(gobj, event, kw, src)
|
|
|
3634
3654
|
let modified = set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, kw);
|
|
3635
3655
|
apply_form_mode(gobj, $form);
|
|
3636
3656
|
set_changed_stated(gobj, modified);
|
|
3657
|
+
gobj_write_bool_attr(gobj, "edited", false);
|
|
3637
3658
|
gobj_write_bool_attr(gobj, "initialized", true);
|
|
3638
3659
|
|
|
3639
3660
|
return 0;
|
|
@@ -3707,6 +3728,7 @@ function ac_clear_record(gobj, event, kw, src)
|
|
|
3707
3728
|
let $form = $container.querySelector('form');
|
|
3708
3729
|
clear_validation(gobj, $form);
|
|
3709
3730
|
clear_data(gobj, $form);
|
|
3731
|
+
set_edited_by_reader(gobj);
|
|
3710
3732
|
return 0;
|
|
3711
3733
|
}
|
|
3712
3734
|
|
|
@@ -3820,7 +3842,7 @@ function ac_add_table_row(gobj, event, kw, src)
|
|
|
3820
3842
|
function ac_record_changed(gobj, event, kw, src)
|
|
3821
3843
|
{
|
|
3822
3844
|
if(gobj_read_bool_attr(gobj, "initialized")) {
|
|
3823
|
-
|
|
3845
|
+
set_edited_by_reader(gobj);
|
|
3824
3846
|
}
|
|
3825
3847
|
return 0;
|
|
3826
3848
|
}
|
|
@@ -3854,6 +3876,7 @@ function ac_paste_record(gobj, event, kw, src)
|
|
|
3854
3876
|
if(is_object(record)) {
|
|
3855
3877
|
gobj_write_bool_attr(gobj, "initialized", true);
|
|
3856
3878
|
set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, record);
|
|
3879
|
+
set_edited_by_reader(gobj);
|
|
3857
3880
|
}
|
|
3858
3881
|
return 0;
|
|
3859
3882
|
}
|
|
@@ -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;
|
|
2558
|
-
*
|
|
2559
|
-
*
|
|
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,197 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_close_asks_for_edits.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* Closing a C_YUI_FORM asks "all changes will be lost" only for what
|
|
5
|
+
* the READER changed. A field the record lacks is filled with its
|
|
6
|
+
* default when the record is loaded: the form then differs from what
|
|
7
|
+
* is stored, and its save lights (`changes`) -- but the reader did
|
|
8
|
+
* nothing, and the close asked all the same. In estadodelaire that
|
|
9
|
+
* was every device whose alarm colours a save had wiped (16 of 19),
|
|
10
|
+
* on every close of its settings. What the reader does -- types,
|
|
11
|
+
* clears, pastes -- is `edited`, and that is what the close asks
|
|
12
|
+
* about.
|
|
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
|
+
if(typeof globalThis.ResizeObserver === "undefined") {
|
|
23
|
+
globalThis.ResizeObserver = class {
|
|
24
|
+
observe() {}
|
|
25
|
+
unobserve() {}
|
|
26
|
+
disconnect() {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
SDATA_END, gclass_create,
|
|
32
|
+
gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
|
|
33
|
+
gobj_start, gobj_send_event, gobj_read_attr,
|
|
34
|
+
set_log_callback,
|
|
35
|
+
} = await import("@yuneta/gobj-js");
|
|
36
|
+
const {register_c_yui_form} = await import("./c_yui_form.js");
|
|
37
|
+
|
|
38
|
+
const logged = [];
|
|
39
|
+
let yuno = null;
|
|
40
|
+
|
|
41
|
+
/* The settings template of an estadodelaire device type, cut down: a
|
|
42
|
+
* field with a default beside a sub-form of three. */
|
|
43
|
+
const TEMPLATE = {
|
|
44
|
+
template_version: {
|
|
45
|
+
type: "integer",
|
|
46
|
+
default: 7,
|
|
47
|
+
flag: ["hidden", "persistent"]
|
|
48
|
+
},
|
|
49
|
+
alarm_colors: {
|
|
50
|
+
type: "object",
|
|
51
|
+
flag: ["writable", "persistent", "template"],
|
|
52
|
+
template: {
|
|
53
|
+
active_not_validated: {
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "red",
|
|
56
|
+
flag: ["writable", "persistent", "color"]
|
|
57
|
+
},
|
|
58
|
+
active_validated: {
|
|
59
|
+
type: "string",
|
|
60
|
+
default: "#e5a50a",
|
|
61
|
+
flag: ["writable", "persistent", "color"]
|
|
62
|
+
},
|
|
63
|
+
not_active_not_validated: {
|
|
64
|
+
type: "string",
|
|
65
|
+
default: "Fuchsia",
|
|
66
|
+
flag: ["writable", "persistent", "color"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const COMPLETE = {
|
|
73
|
+
template_version: 7,
|
|
74
|
+
alarm_colors: {
|
|
75
|
+
active_not_validated: "#ff0000",
|
|
76
|
+
active_validated: "#e5a50a",
|
|
77
|
+
not_active_not_validated: "#ff00ff"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
beforeAll(async () => {
|
|
82
|
+
const i18next = (await import("i18next")).default;
|
|
83
|
+
await i18next.init({lng: "en", resources: {}});
|
|
84
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
85
|
+
set_log_callback((level, msg) => {
|
|
86
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
87
|
+
});
|
|
88
|
+
gclass_create("C_TEST_CLOSE_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
89
|
+
gclass_create(
|
|
90
|
+
"C_TEST_CLOSE_APP",
|
|
91
|
+
[["EV_SAVE_RECORD", 0]],
|
|
92
|
+
[["ST_IDLE", [
|
|
93
|
+
["EV_SAVE_RECORD", () => 0, null],
|
|
94
|
+
]]],
|
|
95
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
96
|
+
);
|
|
97
|
+
register_c_yui_form();
|
|
98
|
+
yuno = gobj_create_yuno("form_close_yuno", "C_TEST_CLOSE_HOST", {});
|
|
99
|
+
gobj_start(yuno);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
logged.length = 0;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let seq = 0;
|
|
107
|
+
|
|
108
|
+
function build(record)
|
|
109
|
+
{
|
|
110
|
+
const app = gobj_create_pure_child(`capp${++seq}`, "C_TEST_CLOSE_APP", {}, yuno);
|
|
111
|
+
gobj_start(app);
|
|
112
|
+
const form = gobj_create_pure_child(`cform${seq}`, "C_YUI_FORM", {
|
|
113
|
+
template: TEMPLATE,
|
|
114
|
+
record: record,
|
|
115
|
+
editable: true,
|
|
116
|
+
}, app);
|
|
117
|
+
gobj_start(form);
|
|
118
|
+
return form;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* What a window or a sheet asks the form before it closes. */
|
|
122
|
+
function close_asks(form)
|
|
123
|
+
{
|
|
124
|
+
const kw = {abort_close: false};
|
|
125
|
+
gobj_send_event(form, "EV_WINDOW_TO_CLOSE", kw, form);
|
|
126
|
+
return kw.abort_close ? (kw.warning || "(no warning)") : "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function errors()
|
|
130
|
+
{
|
|
131
|
+
return logged.filter(l => /error/i.test(l.level) || /ERROR/.test(l.msg));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
describe("the close of a form asks for what the reader changed", () => {
|
|
135
|
+
test("a record that lacks a defaulted field opens changed, and closes without asking", () => {
|
|
136
|
+
const form = build({template_version: 7, alarm_colors: null});
|
|
137
|
+
expect(gobj_read_attr(form, "changes")).toBe(true);
|
|
138
|
+
expect(gobj_read_attr(form, "edited")).toBe(false);
|
|
139
|
+
expect(close_asks(form)).toBe("");
|
|
140
|
+
expect(errors()).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a change of the reader makes it ask", () => {
|
|
144
|
+
const form = build(COMPLETE);
|
|
145
|
+
expect(gobj_read_attr(form, "changes")).toBe(false);
|
|
146
|
+
expect(close_asks(form)).toBe("");
|
|
147
|
+
|
|
148
|
+
gobj_send_event(form, "EV_RECORD_CHANGED", {}, form);
|
|
149
|
+
expect(gobj_read_attr(form, "changes")).toBe(true);
|
|
150
|
+
expect(gobj_read_attr(form, "edited")).toBe(true);
|
|
151
|
+
expect(close_asks(form)).toBe("all changes will be lost");
|
|
152
|
+
expect(errors()).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("an undo, a save or a record loaded end what the close would lose", () => {
|
|
156
|
+
const form = build(COMPLETE);
|
|
157
|
+
|
|
158
|
+
gobj_send_event(form, "EV_RECORD_CHANGED", {}, form);
|
|
159
|
+
gobj_send_event(form, "EV_UNDO_RECORD", {}, form);
|
|
160
|
+
expect(gobj_read_attr(form, "edited")).toBe(false);
|
|
161
|
+
expect(close_asks(form)).toBe("");
|
|
162
|
+
|
|
163
|
+
gobj_send_event(form, "EV_RECORD_CHANGED", {}, form);
|
|
164
|
+
gobj_send_event(form, "EV_SAVE_RECORD", {}, form);
|
|
165
|
+
expect(gobj_read_attr(form, "edited")).toBe(false);
|
|
166
|
+
expect(close_asks(form)).toBe("");
|
|
167
|
+
|
|
168
|
+
gobj_send_event(form, "EV_RECORD_CHANGED", {}, form);
|
|
169
|
+
gobj_send_event(form, "EV_LOAD_RECORD", {template_version: 7, alarm_colors: null}, form);
|
|
170
|
+
expect(gobj_read_attr(form, "changes")).toBe(true); // the defaults again
|
|
171
|
+
expect(gobj_read_attr(form, "edited")).toBe(false);
|
|
172
|
+
expect(close_asks(form)).toBe("");
|
|
173
|
+
expect(errors()).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("clearing and pasting are changes of the reader", () => {
|
|
177
|
+
const cleared = build(COMPLETE);
|
|
178
|
+
gobj_send_event(cleared, "EV_CLEAR_RECORD", {}, cleared);
|
|
179
|
+
expect(gobj_read_attr(cleared, "changes")).toBe(true);
|
|
180
|
+
expect(gobj_read_attr(cleared, "edited")).toBe(true);
|
|
181
|
+
expect(close_asks(cleared)).toBe("all changes will be lost");
|
|
182
|
+
|
|
183
|
+
const pasted = build(COMPLETE);
|
|
184
|
+
gobj_send_event(pasted, "EV_PASTE_RECORD", {record: {
|
|
185
|
+
template_version: 7,
|
|
186
|
+
alarm_colors: {
|
|
187
|
+
active_not_validated: "#00ff00",
|
|
188
|
+
active_validated: "#e5a50a",
|
|
189
|
+
not_active_not_validated: "#ff00ff"
|
|
190
|
+
}
|
|
191
|
+
}}, pasted);
|
|
192
|
+
expect(gobj_read_attr(pasted, "changes")).toBe(true);
|
|
193
|
+
expect(gobj_read_attr(pasted, "edited")).toBe(true);
|
|
194
|
+
expect(close_asks(pasted)).toBe("all changes will be lost");
|
|
195
|
+
expect(errors()).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -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
|
+
});
|
package/src/shell_modals.js
CHANGED
|
@@ -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
|
-
|
|
435
|
+
if(!closed) {
|
|
436
|
+
overlay = yui_shell_register_overlay(shell, on_back);
|
|
437
|
+
}
|
|
419
438
|
});
|
|
420
439
|
};
|
|
421
440
|
|