@yuneta/gobj-ui 7.25.7 → 7.25.9
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 +60 -1
- package/dist/gobj-ui.cjs.js +355 -101
- package/dist/gobj-ui.es.js +355 -101
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +384 -77
- package/src/schema_editor_reload.wiring.test.js +787 -0
- package/src/shell_modals.js +58 -33
- package/src/shell_toast_once.test.js +106 -3
package/src/shell_modals.js
CHANGED
|
@@ -113,12 +113,20 @@ function maybe_apply_translator($node, opts)
|
|
|
113
113
|
* Returns `{ close() }` so callers can dismiss programmatically.
|
|
114
114
|
*
|
|
115
115
|
* ONE toast per message on screen: a string message of the same
|
|
116
|
-
* kind as a toast still showing is not stacked under it
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
116
|
+
* kind as a toast still showing is not stacked under it. One
|
|
117
|
+
* close of a transport settles every request it cut, each view
|
|
118
|
+
* shows its failure, and the operator got a column of identical
|
|
119
|
+
* "the connection dropped". A message built of nodes is never
|
|
120
|
+
* merged: its text is not a key.
|
|
121
|
+
*
|
|
122
|
+
* A repeat is still a CALLER of its own: it gets its own handle
|
|
123
|
+
* and its own time, and the toast stays while any caller holds
|
|
124
|
+
* it -- until every handle is closed or has timed out. Handed the
|
|
125
|
+
* first caller's handle and timer, a repeat asking `timeout: 0`
|
|
126
|
+
* was dismissed on the first one's timer, and closing one
|
|
127
|
+
* caller's handle closed a toast another was still showing
|
|
128
|
+
* (third independent review). The operator's ✕ closes it for
|
|
129
|
+
* everybody.
|
|
122
130
|
***************************************************************/
|
|
123
131
|
function show_notification(shell, kind, message, opts)
|
|
124
132
|
{
|
|
@@ -128,12 +136,12 @@ function show_notification(shell, kind, message, opts)
|
|
|
128
136
|
return { close: () => {} };
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
let timeout = (opts && opts.timeout != null) ? opts.timeout : 5000;
|
|
131
140
|
let key = (typeof message === "string") ? `${kind}\u0000${message}` : null;
|
|
132
141
|
if(key !== null) {
|
|
133
142
|
for(let $shown of Array.from($layer.children)) {
|
|
134
143
|
if($shown.__toast_key__ === key && $shown.__toast__) {
|
|
135
|
-
$shown.__toast__.
|
|
136
|
-
return $shown.__toast__.handle;
|
|
144
|
+
return $shown.__toast__.hold(timeout);
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
}
|
|
@@ -145,7 +153,8 @@ function show_notification(shell, kind, message, opts)
|
|
|
145
153
|
["div", {class: `TOAST notification yui-notification ${yui_tint(kind)}`,
|
|
146
154
|
role: kind === "danger" ? "alert" : "status"},
|
|
147
155
|
[
|
|
148
|
-
["button", {class: "TOAST_CLOSE delete",
|
|
156
|
+
["button", {class: "TOAST_CLOSE delete", title: t("close"), "data-i18n-title": "close",
|
|
157
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"}],
|
|
149
158
|
["p", {...p_attrs, class: "TOAST_MSG"}, message]
|
|
150
159
|
]
|
|
151
160
|
]
|
|
@@ -153,44 +162,57 @@ function show_notification(shell, kind, message, opts)
|
|
|
153
162
|
$layer.appendChild($note);
|
|
154
163
|
maybe_apply_translator($note, opts);
|
|
155
164
|
|
|
156
|
-
let
|
|
165
|
+
let holders = new Set();
|
|
157
166
|
let closed = false;
|
|
158
167
|
|
|
159
|
-
let
|
|
168
|
+
let close_toast = function() {
|
|
160
169
|
if(closed) {
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
163
172
|
closed = true;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
for(let holder of holders) {
|
|
174
|
+
if(holder.timeout_id) {
|
|
175
|
+
clearTimeout(holder.timeout_id);
|
|
176
|
+
holder.timeout_id = null;
|
|
177
|
+
}
|
|
167
178
|
}
|
|
179
|
+
holders.clear();
|
|
168
180
|
if($note.parentNode) {
|
|
169
181
|
$note.parentNode.removeChild($note);
|
|
170
182
|
}
|
|
171
183
|
};
|
|
172
184
|
|
|
185
|
+
/* One caller of this toast: its handle, and its time. */
|
|
186
|
+
let hold = function(ms) {
|
|
187
|
+
let holder = {timeout_id: null};
|
|
188
|
+
let release = function() {
|
|
189
|
+
if(!holders.has(holder)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if(holder.timeout_id) {
|
|
193
|
+
clearTimeout(holder.timeout_id);
|
|
194
|
+
holder.timeout_id = null;
|
|
195
|
+
}
|
|
196
|
+
holders.delete(holder);
|
|
197
|
+
if(holders.size === 0) {
|
|
198
|
+
close_toast();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
holders.add(holder);
|
|
202
|
+
if(ms > 0) {
|
|
203
|
+
holder.timeout_id = setTimeout(release, ms);
|
|
204
|
+
}
|
|
205
|
+
return { close: release };
|
|
206
|
+
};
|
|
207
|
+
|
|
173
208
|
let $del = $note.querySelector(".delete");
|
|
174
209
|
if($del) {
|
|
175
|
-
$del.addEventListener("click",
|
|
210
|
+
$del.addEventListener("click", close_toast);
|
|
176
211
|
}
|
|
177
212
|
|
|
178
|
-
let timeout = (opts && opts.timeout != null) ? opts.timeout : 5000;
|
|
179
|
-
let restart = function() {
|
|
180
|
-
if(timeout_id) {
|
|
181
|
-
clearTimeout(timeout_id);
|
|
182
|
-
timeout_id = null;
|
|
183
|
-
}
|
|
184
|
-
if(timeout > 0) {
|
|
185
|
-
timeout_id = setTimeout(close, timeout);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
restart();
|
|
189
|
-
|
|
190
|
-
let handle = { close };
|
|
191
213
|
$note.__toast_key__ = key;
|
|
192
|
-
$note.__toast__ = {
|
|
193
|
-
return
|
|
214
|
+
$note.__toast__ = {hold: hold};
|
|
215
|
+
return hold(timeout);
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
|
|
@@ -263,7 +285,8 @@ export function yui_shell_show_modal(shell, content, opts)
|
|
|
263
285
|
.concat(title
|
|
264
286
|
? [["span", {class: "MODAL_TITLE_KIND", i18n: title}, title]]
|
|
265
287
|
: [])],
|
|
266
|
-
["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button",
|
|
288
|
+
["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button", title: t("close"), "data-i18n-title": "close",
|
|
289
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"},
|
|
267
290
|
[["i", {class: "yi-xmark"}]]],
|
|
268
291
|
]];
|
|
269
292
|
let body = ["div", {class: "MODAL_BODY yui-dialog-body"}, inner ? [inner] : []];
|
|
@@ -283,7 +306,8 @@ export function yui_shell_show_modal(shell, content, opts)
|
|
|
283
306
|
if(with_close) {
|
|
284
307
|
modal_children.push(
|
|
285
308
|
["button", {class: "MODAL_CLOSE modal-close is-large",
|
|
286
|
-
|
|
309
|
+
title: t("close"), "data-i18n-title": "close",
|
|
310
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"}]
|
|
287
311
|
);
|
|
288
312
|
}
|
|
289
313
|
}
|
|
@@ -498,7 +522,8 @@ function build_dialog(shell, message, buttons, opts)
|
|
|
498
522
|
);
|
|
499
523
|
}
|
|
500
524
|
$card_children.push(
|
|
501
|
-
["button", {class: "CONFIRM_CLOSE delete yui-confirm-x",
|
|
525
|
+
["button", {class: "CONFIRM_CLOSE delete yui-confirm-x", title: t("close"), "data-i18n-title": "close",
|
|
526
|
+
"aria-label": t("close"), "data-i18n-aria-label": "close"}],
|
|
502
527
|
["section", {class: "CONFIRM_BODY modal-card-body has-text-centered"},
|
|
503
528
|
$body_children],
|
|
504
529
|
["footer", {class: "CONFIRM_FOOT modal-card-foot"}, $footer_children]
|
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
* the operator got a column of identical "the connection
|
|
8
8
|
* dropped" (low of the independent review of the 2nd round).
|
|
9
9
|
*
|
|
10
|
+
* And a repeat is a caller of its own (third independent
|
|
11
|
+
* review): its handle and its time are its own, and the toast
|
|
12
|
+
* stays while any caller still holds it.
|
|
13
|
+
*
|
|
14
|
+
* And every ✕ this module draws is a control with a NAME: a
|
|
15
|
+
* `title` and an `aria-label`, both translatable (fourth
|
|
16
|
+
* independent review: TOAST_CLOSE had the label only).
|
|
17
|
+
*
|
|
10
18
|
* Copyright (c) 2026, ArtGins.
|
|
11
19
|
* All Rights Reserved.
|
|
12
20
|
***********************************************************************/
|
|
13
|
-
import {describe, test, expect, beforeEach} from "vitest";
|
|
21
|
+
import {describe, test, expect, beforeEach, afterEach, vi} from "vitest";
|
|
14
22
|
import {install_dom_double} from "../test/dom_double.js";
|
|
15
23
|
|
|
16
24
|
install_dom_double();
|
|
@@ -18,6 +26,8 @@ install_dom_double();
|
|
|
18
26
|
const {
|
|
19
27
|
yui_shell_show_error,
|
|
20
28
|
yui_shell_show_info,
|
|
29
|
+
yui_shell_show_modal,
|
|
30
|
+
yui_shell_confirm_danger,
|
|
21
31
|
} = await import("./shell_modals.js");
|
|
22
32
|
|
|
23
33
|
let shell = null;
|
|
@@ -55,10 +65,103 @@ describe("one toast per message on screen", () => {
|
|
|
55
65
|
expect(toasts().length).toBe(1);
|
|
56
66
|
});
|
|
57
67
|
|
|
58
|
-
test("the
|
|
59
|
-
yui_shell_show_error(shell, "the connection dropped");
|
|
68
|
+
test("the toast goes when EVERY handle of it is closed", () => {
|
|
69
|
+
const first = yui_shell_show_error(shell, "the connection dropped");
|
|
60
70
|
const again = yui_shell_show_error(shell, "the connection dropped");
|
|
61
71
|
again.close();
|
|
72
|
+
expect(toasts().length).toBe(1); /* the first still holds it */
|
|
73
|
+
first.close();
|
|
74
|
+
expect(toasts().length).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* The third independent review: a repeat was handed the FIRST toast's
|
|
80
|
+
* handle and timer. A repeat asking `timeout: 0` was dismissed on the
|
|
81
|
+
* first one's timer, and closing one caller's handle closed the toast
|
|
82
|
+
* another caller was still showing.
|
|
83
|
+
*/
|
|
84
|
+
describe("a repeat is its own caller", () => {
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
vi.useFakeTimers();
|
|
88
|
+
});
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
vi.useRealTimers();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a repeat asking timeout 0 is not dismissed on the first one's timer", () => {
|
|
94
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 1000});
|
|
95
|
+
const sticky = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
96
|
+
vi.advanceTimersByTime(5000);
|
|
97
|
+
expect(toasts().length).toBe(1);
|
|
98
|
+
sticky.close();
|
|
99
|
+
expect(toasts().length).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("closing the first caller's handle leaves the repeat's toast up", () => {
|
|
103
|
+
const first = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
104
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 3000});
|
|
105
|
+
first.close();
|
|
106
|
+
expect(toasts().length).toBe(1);
|
|
107
|
+
vi.advanceTimersByTime(3000);
|
|
62
108
|
expect(toasts().length).toBe(0);
|
|
63
109
|
});
|
|
110
|
+
|
|
111
|
+
test("the longest time wins, as a restart did", () => {
|
|
112
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 1000});
|
|
113
|
+
vi.advanceTimersByTime(800);
|
|
114
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 1000});
|
|
115
|
+
vi.advanceTimersByTime(500);
|
|
116
|
+
expect(toasts().length).toBe(1);
|
|
117
|
+
vi.advanceTimersByTime(500);
|
|
118
|
+
expect(toasts().length).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("the operator's close takes it down for every caller", () => {
|
|
122
|
+
const first = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
123
|
+
const again = yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
124
|
+
toasts()[0].querySelector(".TOAST_CLOSE").click();
|
|
125
|
+
expect(toasts().length).toBe(0);
|
|
126
|
+
first.close();
|
|
127
|
+
again.close();
|
|
128
|
+
yui_shell_show_error(shell, "the connection dropped", {timeout: 0});
|
|
129
|
+
expect(toasts().length).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("every close control is named", () => {
|
|
134
|
+
|
|
135
|
+
function named($b)
|
|
136
|
+
{
|
|
137
|
+
/* The text itself is whatever the app's i18next answers (none
|
|
138
|
+
* here); what is checked is that both are THERE and carry the
|
|
139
|
+
* key a language change re-translates them by. */
|
|
140
|
+
return {
|
|
141
|
+
title: $b.getAttribute("title") !== null,
|
|
142
|
+
title_key: $b.getAttribute("data-i18n-title"),
|
|
143
|
+
label: $b.getAttribute("aria-label") !== null,
|
|
144
|
+
label_key: $b.getAttribute("data-i18n-aria-label"),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const NAMED = {title: true, title_key: "close", label: true, label_key: "close"};
|
|
149
|
+
|
|
150
|
+
test("the toast's", () => {
|
|
151
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
152
|
+
expect(named(toasts()[0].querySelector(".TOAST_CLOSE"))).toEqual(NAMED);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a modal's, dialog or plain, and a confirmation's", () => {
|
|
156
|
+
shell.priv.layers.modal = document.createElement("div");
|
|
157
|
+
yui_shell_show_modal(shell, document.createElement("div"), {dialog: true, title: "x"});
|
|
158
|
+
yui_shell_show_modal(shell, document.createElement("div"), {});
|
|
159
|
+
yui_shell_confirm_danger(shell, "delete this column?");
|
|
160
|
+
const $closes = shell.priv.layers.modal.querySelectorAll(
|
|
161
|
+
".MODAL_CLOSE, .CONFIRM_CLOSE");
|
|
162
|
+
expect($closes.length).toBe(3);
|
|
163
|
+
for(const $b of $closes) {
|
|
164
|
+
expect(named($b)).toEqual(NAMED);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
64
167
|
});
|