@arraypress/extension-kit 1.9.0 → 1.11.0
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 +145 -18
- package/bin/extension-kit.js +2 -1
- package/package.json +1 -1
- package/src/ai/host.d.ts +183 -0
- package/src/ai/host.js +225 -0
- package/src/ai/model.d.ts +2 -1
- package/src/ai/model.js +10 -2
- package/src/build/index.d.ts +2 -1
- package/src/build/index.js +2 -1
- package/src/build/store-shots.d.ts +177 -0
- package/src/build/store-shots.js +173 -0
- package/src/build/test-build.d.ts +13 -1
- package/src/build/test-build.js +41 -2
- package/src/build/vendor.d.ts +29 -8
- package/src/build/vendor.js +136 -40
- package/src/content/elements.d.ts +50 -0
- package/src/content/elements.js +88 -0
- package/src/content/highlight.d.ts +63 -0
- package/src/content/highlight.js +117 -0
- package/src/content/overlay.d.ts +31 -6
- package/src/content/overlay.js +56 -8
- package/src/content/page-toast.d.ts +26 -0
- package/src/content/page-toast.js +72 -0
- package/src/content/slices.d.ts +3 -1
- package/src/content/slices.js +29 -14
- package/src/content/watch.d.ts +4 -2
- package/src/content/watch.js +17 -3
- package/src/dom/copy.d.ts +36 -0
- package/src/dom/copy.js +84 -0
- package/src/dom/focus.d.ts +11 -0
- package/src/dom/focus.js +15 -0
- package/src/dom/i18n.d.ts +17 -0
- package/src/dom/i18n.js +15 -0
- package/src/dom/make.d.ts +53 -0
- package/src/dom/make.js +71 -0
- package/src/frame/window.d.ts +8 -1
- package/src/frame/window.js +11 -8
- package/src/index.d.ts +31 -6
- package/src/index.js +24 -6
- package/src/panel/show-on-page.d.ts +82 -0
- package/src/panel/show-on-page.js +83 -0
- package/src/scripting/call.d.ts +29 -9
- package/src/scripting/call.js +35 -11
- package/src/scripting/context-menus.d.ts +45 -0
- package/src/scripting/context-menus.js +53 -0
- package/src/scripting/registration.d.ts +2 -1
- package/src/scripting/registration.js +2 -1
- package/src/scripting/tab.d.ts +80 -0
- package/src/scripting/tab.js +89 -0
- package/src/settings/saver.js +16 -6
- package/src/settings/storage.d.ts +33 -2
- package/src/settings/storage.js +73 -3
- package/src/settings/store.d.ts +17 -0
- package/src/settings/store.js +87 -20
- package/src/sites/sites.d.ts +6 -1
- package/src/sites/sites.js +20 -3
- package/src/styles/chrome-ui.css +63 -30
- package/src/testing/e2e.d.ts +35 -4
- package/src/testing/e2e.js +67 -5
- package/src/testing/fake-model.d.ts +9 -1
- package/src/testing/fake-model.js +44 -23
- package/src/testing/fake-storage.d.ts +41 -13
- package/src/testing/fake-storage.js +163 -51
- package/src/testing/index.d.ts +8 -2
- package/src/testing/index.js +8 -2
- package/src/testing/live-run.d.ts +151 -0
- package/src/testing/live-run.js +206 -0
- package/src/testing/messages.d.ts +7 -3
- package/src/testing/messages.js +50 -9
- package/src/testing/package.d.ts +56 -7
- package/src/testing/package.js +57 -18
- package/src/testing/pages.d.ts +114 -0
- package/src/testing/pages.js +109 -0
- package/src/testing/prompt.d.ts +4 -2
- package/src/testing/prompt.js +7 -3
- package/src/time/pool.d.ts +2 -1
- package/src/time/pool.js +4 -2
- package/src/toolbar/theme.d.ts +10 -2
- package/src/toolbar/theme.js +12 -4
- package/src/ui/menu.d.ts +13 -3
- package/src/ui/menu.js +31 -11
- package/src/ui/model-row.js +5 -1
- package/src/ui/notice.d.ts +58 -0
- package/src/ui/notice.js +43 -0
- package/src/ui/site-list.js +12 -3
- package/src/ui/text-dialog.d.ts +3 -2
- package/src/ui/text-dialog.js +10 -3
- package/src/ui/toast.js +2 -0
package/src/ui/menu.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module ui/menu
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { focusedElement, rootDocument } from '../dom/focus.js';
|
|
17
|
+
import { focusedElement, focusedIn, rootDocument } from '../dom/focus.js';
|
|
18
18
|
|
|
19
19
|
/** The gap kept between a menu and the window's edges, in px. */
|
|
20
20
|
export const MENU_EDGE = 8;
|
|
@@ -67,19 +67,21 @@ const BELOW_GAP = 4;
|
|
|
67
67
|
*
|
|
68
68
|
* @param {{width: number, height: number}} size - The menu's size.
|
|
69
69
|
* @param {{width: number, height: number}} viewport - The window's (`innerWidth`, `innerHeight`).
|
|
70
|
-
* @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below'}} where - The
|
|
71
|
-
* box, or a point; `placement` says where it goes from an anchor (default `'over'`)
|
|
70
|
+
* @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below', rtl?: boolean}} where - The
|
|
71
|
+
* anchor's box, or a point; `placement` says where it goes from an anchor (default `'over'`); `rtl`
|
|
72
|
+
* (a right-to-left page) lines its start up with the anchor's left edge and opens leftwards from a
|
|
73
|
+
* point, as Chrome mirrors its own.
|
|
72
74
|
* @returns {{left: number, top: number}} The menu's top-left, in px.
|
|
73
75
|
* @example
|
|
74
76
|
* menuPosition({width: 160, height: 120}, {width: 800, height: 600}, {anchor: button.getBoundingClientRect()});
|
|
75
77
|
* // → {left: anchor.right - 160, top: anchor.top}
|
|
76
78
|
*/
|
|
77
|
-
export function menuPosition(size, viewport, { anchor, x = 0, y = 0, placement = 'over' }) {
|
|
79
|
+
export function menuPosition(size, viewport, { anchor, x = 0, y = 0, placement = 'over', rtl = false }) {
|
|
78
80
|
const { width, height } = size;
|
|
79
81
|
const left = (/** @type {number} */ start) => Math.max(MENU_EDGE, Math.min(start, viewport.width - width - MENU_EDGE));
|
|
80
82
|
const room = (/** @type {number} */ top) => top + height <= viewport.height - MENU_EDGE;
|
|
81
|
-
if (!anchor) return { left: left(x), top: Math.max(MENU_EDGE, room(y) ? y : y - height) };
|
|
82
|
-
const start = left(anchor.right - width);
|
|
83
|
+
if (!anchor) return { left: left(rtl ? x - width : x), top: Math.max(MENU_EDGE, room(y) ? y : y - height) };
|
|
84
|
+
const start = left(rtl ? anchor.left : anchor.right - width);
|
|
83
85
|
if (placement === 'below') {
|
|
84
86
|
const below = anchor.bottom + BELOW_GAP;
|
|
85
87
|
return { left: start, top: Math.max(MENU_EDGE, room(below) ? below : anchor.top - height - BELOW_GAP) };
|
|
@@ -118,6 +120,8 @@ export function isInside(box, x, y) {
|
|
|
118
120
|
* @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
|
|
119
121
|
* and open it (closing it first if it's open).
|
|
120
122
|
* @property {() => void} close Close it, if open, and return the focus.
|
|
123
|
+
* @property {() => void} destroy Take it off the page and stop listening to the window (a content
|
|
124
|
+
* script's overlay being removed).
|
|
121
125
|
*/
|
|
122
126
|
|
|
123
127
|
/**
|
|
@@ -168,21 +172,32 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
|
|
|
168
172
|
|
|
169
173
|
menu.addEventListener('keydown', (event) => {
|
|
170
174
|
const list = items();
|
|
171
|
-
|
|
175
|
+
// Asked of the menu's own root: a closed shadow root's focused element can't be seen from the document.
|
|
176
|
+
const at = list.indexOf(/** @type {HTMLButtonElement} */ (focusedIn(menu)));
|
|
172
177
|
/** @param {number} index */
|
|
173
178
|
const go = (index) => {
|
|
174
|
-
|
|
179
|
+
handled();
|
|
175
180
|
list[(index + list.length) % list.length]?.focus();
|
|
176
181
|
};
|
|
182
|
+
/** The key is the menu's: the page under a content script's menu doesn't act on it too (its own shortcuts). */
|
|
183
|
+
const handled = () => {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
};
|
|
177
187
|
if (event.key === 'ArrowDown') go(at + 1);
|
|
178
188
|
else if (event.key === 'ArrowUp') go(at < 0 ? -1 : at - 1);
|
|
179
189
|
else if (event.key === 'Home') go(0);
|
|
180
190
|
else if (event.key === 'End') go(-1);
|
|
181
191
|
else if (event.key === 'Escape' || event.key === 'Tab') {
|
|
182
|
-
|
|
192
|
+
handled();
|
|
183
193
|
close();
|
|
184
194
|
}
|
|
185
195
|
});
|
|
196
|
+
// Closed any other way (the browser's own close, another script): the button no longer says it's open.
|
|
197
|
+
menu.addEventListener('close', () => {
|
|
198
|
+
expanded?.setAttribute('aria-expanded', 'false');
|
|
199
|
+
expanded = null;
|
|
200
|
+
});
|
|
186
201
|
// Esc the browser handles itself (when the keydown above didn't see it).
|
|
187
202
|
menu.addEventListener('cancel', (event) => {
|
|
188
203
|
event.preventDefault();
|
|
@@ -219,7 +234,7 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
|
|
|
219
234
|
if (label) menu.setAttribute('aria-label', label);
|
|
220
235
|
else menu.removeAttribute('aria-label');
|
|
221
236
|
|
|
222
|
-
const active = focusedElement(doc);
|
|
237
|
+
const active = focusedIn(menu) ?? focusedElement(doc);
|
|
223
238
|
returnFocus = focusTo ?? anchor ?? (active instanceof view.HTMLElement ? active : null);
|
|
224
239
|
if (anchor?.hasAttribute('aria-haspopup')) {
|
|
225
240
|
anchor.setAttribute('aria-expanded', 'true');
|
|
@@ -228,13 +243,18 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
|
|
|
228
243
|
menu.showModal();
|
|
229
244
|
const { width, height } = menu.getBoundingClientRect();
|
|
230
245
|
const { left, top } = menuPosition({ width, height }, { width: view.innerWidth, height: view.innerHeight }, {
|
|
231
|
-
anchor: anchor?.getBoundingClientRect(), x, y, placement,
|
|
246
|
+
anchor: anchor?.getBoundingClientRect(), x, y, placement, rtl: view.getComputedStyle(menu).direction === 'rtl',
|
|
232
247
|
});
|
|
233
248
|
menu.style.left = `${left}px`;
|
|
234
249
|
menu.style.top = `${top}px`;
|
|
235
250
|
items()[0]?.focus();
|
|
236
251
|
},
|
|
237
252
|
close,
|
|
253
|
+
destroy() {
|
|
254
|
+
shut();
|
|
255
|
+
view.removeEventListener('blur', close);
|
|
256
|
+
menu.remove();
|
|
257
|
+
},
|
|
238
258
|
};
|
|
239
259
|
}
|
|
240
260
|
|
package/src/ui/model-row.js
CHANGED
|
@@ -94,7 +94,11 @@ export function createModelRow({ status, help, download, progress }, { options,
|
|
|
94
94
|
progress.value = loaded;
|
|
95
95
|
status.textContent = t(MODEL_MESSAGES.downloading, [String(Math.round(loaded * 100))]);
|
|
96
96
|
},
|
|
97
|
-
}).then(
|
|
97
|
+
}).then(
|
|
98
|
+
// The state as the code that uses it sees it; if it can't be asked (the worker didn't answer), as this page does.
|
|
99
|
+
async (here) => show(await state().catch(() => here)),
|
|
100
|
+
() => show('downloadable', t(MODEL_MESSAGES.downloadFailed)),
|
|
101
|
+
);
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
return {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A notice in a side panel's list (chrome-ui.css `.sp-notice`): a card with an icon, a heading,
|
|
3
|
+
* a line, buttons beside the heading and actions under the line. For what a list leaves out or
|
|
4
|
+
* can't do: frames from other sites to allow, a web font that didn't load.
|
|
5
|
+
*
|
|
6
|
+
* @module ui/notice
|
|
7
|
+
*/
|
|
8
|
+
export type Notice = {
|
|
9
|
+
/**
|
|
10
|
+
* Its sprite symbol's id.
|
|
11
|
+
*/
|
|
12
|
+
icon: string;
|
|
13
|
+
/**
|
|
14
|
+
* An information's icon is grey, a warning's red. Default `warning`.
|
|
15
|
+
*/
|
|
16
|
+
tone?: 'info' | 'warning';
|
|
17
|
+
/**
|
|
18
|
+
* The heading.
|
|
19
|
+
*/
|
|
20
|
+
title: string;
|
|
21
|
+
/**
|
|
22
|
+
* The line under it.
|
|
23
|
+
*/
|
|
24
|
+
text: string;
|
|
25
|
+
/**
|
|
26
|
+
* Icon buttons at the heading's end.
|
|
27
|
+
*/
|
|
28
|
+
buttons?: HTMLElement[];
|
|
29
|
+
/**
|
|
30
|
+
* Buttons under the line, at the end (“Allow”).
|
|
31
|
+
*/
|
|
32
|
+
actions?: HTMLElement[];
|
|
33
|
+
/**
|
|
34
|
+
* The page. Default `document`.
|
|
35
|
+
*/
|
|
36
|
+
doc?: Document;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A notice's parts.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} Notice
|
|
42
|
+
* @property {string} icon Its sprite symbol's id.
|
|
43
|
+
* @property {'info' | 'warning'} [tone] An information's icon is grey, a warning's red. Default `warning`.
|
|
44
|
+
* @property {string} title The heading.
|
|
45
|
+
* @property {string} text The line under it.
|
|
46
|
+
* @property {HTMLElement[]} [buttons] Icon buttons at the heading's end.
|
|
47
|
+
* @property {HTMLElement[]} [actions] Buttons under the line, at the end (“Allow”).
|
|
48
|
+
* @property {Document} [doc] The page. Default `document`.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* A notice card.
|
|
52
|
+
*
|
|
53
|
+
* @param {Notice} notice - Its parts.
|
|
54
|
+
* @returns {HTMLElement} The card.
|
|
55
|
+
* @example
|
|
56
|
+
* notices.append(noticeCard({ icon: 'i-frame', tone: 'info', title: t('panelFrames'), text, actions: [allow] }));
|
|
57
|
+
*/
|
|
58
|
+
export declare function noticeCard({ icon, tone, title, text, buttons, actions, doc }: Notice): HTMLElement;
|
package/src/ui/notice.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A notice in a side panel's list (chrome-ui.css `.sp-notice`): a card with an icon, a heading,
|
|
3
|
+
* a line, buttons beside the heading and actions under the line. For what a list leaves out or
|
|
4
|
+
* can't do: frames from other sites to allow, a web font that didn't load.
|
|
5
|
+
*
|
|
6
|
+
* @module ui/notice
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { make, spriteIcon } from '../dom/make.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A notice's parts.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} Notice
|
|
15
|
+
* @property {string} icon Its sprite symbol's id.
|
|
16
|
+
* @property {'info' | 'warning'} [tone] An information's icon is grey, a warning's red. Default `warning`.
|
|
17
|
+
* @property {string} title The heading.
|
|
18
|
+
* @property {string} text The line under it.
|
|
19
|
+
* @property {HTMLElement[]} [buttons] Icon buttons at the heading's end.
|
|
20
|
+
* @property {HTMLElement[]} [actions] Buttons under the line, at the end (“Allow”).
|
|
21
|
+
* @property {Document} [doc] The page. Default `document`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A notice card.
|
|
26
|
+
*
|
|
27
|
+
* @param {Notice} notice - Its parts.
|
|
28
|
+
* @returns {HTMLElement} The card.
|
|
29
|
+
* @example
|
|
30
|
+
* notices.append(noticeCard({ icon: 'i-frame', tone: 'info', title: t('panelFrames'), text, actions: [allow] }));
|
|
31
|
+
*/
|
|
32
|
+
export function noticeCard({ icon, tone = 'warning', title, text, buttons = [], actions = [], doc = document }) {
|
|
33
|
+
const card = make('section', `sp-card sp-notice ${tone}`, '', doc);
|
|
34
|
+
const heading = make('div', 'sp-heading', '', doc);
|
|
35
|
+
heading.append(spriteIcon(icon, '', doc), make('h2', '', title, doc), ...buttons);
|
|
36
|
+
card.append(heading, make('p', 'sp-notice-text', text, doc));
|
|
37
|
+
if (actions.length) {
|
|
38
|
+
const row = make('div', 'sp-notice-actions', '', doc);
|
|
39
|
+
row.append(...actions);
|
|
40
|
+
card.append(row);
|
|
41
|
+
}
|
|
42
|
+
return card;
|
|
43
|
+
}
|
package/src/ui/site-list.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* @module ui/site-list
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { focusedIn } from '../dom/focus.js';
|
|
30
31
|
import { createMenu, moreActionsButton } from './menu.js';
|
|
31
32
|
import { askText } from './text-dialog.js';
|
|
32
33
|
import { MAX_HOST_LENGTH, siteLabel } from '../sites/sites.js';
|
|
@@ -177,9 +178,11 @@ export function createSiteList({
|
|
|
177
178
|
form?.addEventListener('submit', async (event) => {
|
|
178
179
|
event.preventDefault();
|
|
179
180
|
const input = /** @type {HTMLInputElement} */ (form.querySelector('input'));
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
const typed = input.value;
|
|
182
|
+
const site = parse(typed);
|
|
183
|
+
if (!site) invalid?.(typed);
|
|
184
|
+
// Cleared only if it's still what was added: adding can wait on Chrome (a permission prompt), and the next site may be typed meanwhile.
|
|
185
|
+
else if (await add(site) !== false && input.value === typed) input.value = '';
|
|
183
186
|
});
|
|
184
187
|
addButton?.addEventListener('click', async () => {
|
|
185
188
|
const site = await ask(w.addSite, w.add);
|
|
@@ -219,6 +222,11 @@ export function createSiteList({
|
|
|
219
222
|
|
|
220
223
|
return {
|
|
221
224
|
render(sites) {
|
|
225
|
+
// The focus stays in the list across a redraw: on the same row's button (the one after a removed
|
|
226
|
+
// row, the last after the last), or on the way to add when no row is left. Without this, the
|
|
227
|
+
// row's button a dialog or menu gave the focus back to is replaced and the focus falls to the page.
|
|
228
|
+
const focused = focusedIn(list);
|
|
229
|
+
const at = focused && list.contains(focused) ? [...list.children].findIndex((row) => row.contains(focused)) : -1;
|
|
222
230
|
list.replaceChildren(...sites.map((site) => {
|
|
223
231
|
const row = doc.createElement('li');
|
|
224
232
|
row.className = 'row';
|
|
@@ -229,6 +237,7 @@ export function createSiteList({
|
|
|
229
237
|
return row;
|
|
230
238
|
}));
|
|
231
239
|
if (empty) empty.hidden = sites.length > 0;
|
|
240
|
+
if (at >= 0) (list.children[Math.min(at, sites.length - 1)]?.querySelector('button') ?? addButton ?? form?.querySelector('input'))?.focus();
|
|
232
241
|
},
|
|
233
242
|
};
|
|
234
243
|
}
|
package/src/ui/text-dialog.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* folder's name (`askText`); several, as chrome://settings/searchEngines asks
|
|
5
5
|
* for a site search (`askFields`). The action button stays disabled while a
|
|
6
6
|
* field is blank or isn't valid, with the reason under that field. Enter
|
|
7
|
-
* means the action button, Esc and Cancel cancel
|
|
8
|
-
*
|
|
7
|
+
* means the action button, Esc and Cancel cancel (as does the dialog being
|
|
8
|
+
* closed any other way), and the focus goes back to what opened it. A reason
|
|
9
|
+
* is announced as it appears, and its field marked invalid. The page needs no markup: the dialog (`#text-dialog`) is
|
|
9
10
|
* made the first time, its fields each time it's asked.
|
|
10
11
|
*
|
|
11
12
|
* @module ui/text-dialog
|
package/src/ui/text-dialog.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* folder's name (`askText`); several, as chrome://settings/searchEngines asks
|
|
5
5
|
* for a site search (`askFields`). The action button stays disabled while a
|
|
6
6
|
* field is blank or isn't valid, with the reason under that field. Enter
|
|
7
|
-
* means the action button, Esc and Cancel cancel
|
|
8
|
-
*
|
|
7
|
+
* means the action button, Esc and Cancel cancel (as does the dialog being
|
|
8
|
+
* closed any other way), and the focus goes back to what opened it. A reason
|
|
9
|
+
* is announced as it appears, and its field marked invalid. The page needs no markup: the dialog (`#text-dialog`) is
|
|
9
10
|
* made the first time, its fields each time it's asked.
|
|
10
11
|
*
|
|
11
12
|
* @module ui/text-dialog
|
|
@@ -178,7 +179,7 @@ function fieldFor(doc, request, only) {
|
|
|
178
179
|
input.value = typeof request.value === 'string' ? request.value : '';
|
|
179
180
|
if (request.placeholder) input.placeholder = request.placeholder;
|
|
180
181
|
if (request.maxLength !== undefined) input.maxLength = request.maxLength;
|
|
181
|
-
const error = element(doc, 'span', { id: ids.error, class: 'hint error' });
|
|
182
|
+
const error = element(doc, 'span', { id: ids.error, class: 'hint error', 'aria-live': 'polite' });
|
|
182
183
|
error.hidden = true;
|
|
183
184
|
const hint = element(doc, 'span', { id: ids.hint, class: 'hint secondary' });
|
|
184
185
|
hint.hidden = true;
|
|
@@ -226,6 +227,9 @@ export function askFields({
|
|
|
226
227
|
const { reason } = waiting ? { reason: '' } : checkText(input.value, request.validate);
|
|
227
228
|
error.textContent = reason;
|
|
228
229
|
error.hidden = !reason;
|
|
230
|
+
// Said to a screen reader as it appears, and the field marked (the stylesheet turns its underline red).
|
|
231
|
+
if (reason) input.setAttribute('aria-invalid', 'true');
|
|
232
|
+
else input.removeAttribute('aria-invalid');
|
|
229
233
|
hint.textContent = waiting ? request.waiting ?? '' : typeof request.hint === 'function' ? request.hint(input.value) : request.hint ?? '';
|
|
230
234
|
hint.hidden = !hint.textContent;
|
|
231
235
|
};
|
|
@@ -291,6 +295,9 @@ export function askFields({
|
|
|
291
295
|
event.preventDefault();
|
|
292
296
|
settle(null);
|
|
293
297
|
}, { signal });
|
|
298
|
+
// Closed any other way (the browser's own close after a second Esc, another script): cancelled, not left unanswered.
|
|
299
|
+
// (The event comes a task after the closing: one from the ask before this, already answered, finds the dialog open again.)
|
|
300
|
+
parts.dialog.addEventListener('close', () => { if (!parts.dialog.open) settle(null); }, { signal });
|
|
294
301
|
|
|
295
302
|
parts.dialog.showModal();
|
|
296
303
|
// The first field still to fill in, or the first, its text selected to type over.
|
package/src/ui/toast.js
CHANGED
|
@@ -93,6 +93,8 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
|
|
|
93
93
|
text.textContent = message;
|
|
94
94
|
undoButton.hidden = !undo;
|
|
95
95
|
undoLast = undo;
|
|
96
|
+
// Taken off the page since (a single-page site replaced its body): put back, or showing it would throw.
|
|
97
|
+
if (!toast.isConnected) (root?.isConnected === false ? doc.body : root ?? doc.body).append(toast);
|
|
96
98
|
// Hidden and shown again, so it's the newest in the top layer (over a popover since).
|
|
97
99
|
toast.togglePopover(false);
|
|
98
100
|
toast.togglePopover(true);
|