@arraypress/extension-kit 1.1.0 → 1.3.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 +166 -14
- package/bin/extension-kit.js +46 -10
- package/package.json +1 -1
- package/src/build/index.d.ts +4 -2
- package/src/build/index.js +6 -2
- package/src/build/store-frame.d.ts +176 -0
- package/src/build/store-frame.js +241 -0
- package/src/build/test-build.d.ts +85 -0
- package/src/build/test-build.js +82 -0
- package/src/build/toolbar-icons.d.ts +36 -18
- package/src/build/toolbar-icons.js +91 -24
- package/src/build/vendor.d.ts +78 -8
- package/src/build/vendor.js +133 -10
- package/src/build/zip.d.ts +3 -2
- package/src/build/zip.js +6 -4
- package/src/dom/focus.d.ts +28 -0
- package/src/dom/focus.js +36 -0
- package/src/dom/setup.d.ts +54 -0
- package/src/dom/setup.js +46 -0
- package/src/index.d.ts +29 -2
- package/src/index.js +23 -2
- package/src/sites/sites.d.ts +82 -0
- package/src/sites/sites.js +128 -0
- package/src/styles/chrome-ui.css +18 -7
- package/src/testing/browser.d.ts +52 -0
- package/src/testing/browser.js +79 -0
- package/src/testing/e2e.d.ts +18 -5
- package/src/testing/e2e.js +17 -5
- package/src/testing/index.d.ts +15 -4
- package/src/testing/index.js +15 -4
- package/src/testing/package.d.ts +62 -4
- package/src/testing/package.js +39 -8
- package/src/testing/pool.d.ts +23 -0
- package/src/testing/pool.js +33 -0
- package/src/testing/saved-groups.d.ts +33 -0
- package/src/testing/saved-groups.js +222 -0
- package/src/testing/time.d.ts +35 -0
- package/src/testing/time.js +46 -0
- package/src/testing/warnings.d.ts +57 -0
- package/src/testing/warnings.js +69 -0
- package/src/ui/menu.d.ts +257 -0
- package/src/ui/menu.js +283 -0
- package/src/ui/site-list.d.ts +199 -0
- package/src/ui/site-list.js +194 -0
- package/src/ui/text-dialog.d.ts +132 -0
- package/src/ui/text-dialog.js +187 -0
- package/src/ui/toast.d.ts +89 -0
- package/src/ui/toast.js +103 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's one-field dialog (cr-dialog with a cr-input), as chrome://bookmarks
|
|
3
|
+
* asks for a folder's name: a title, a labelled field, Cancel and an action
|
|
4
|
+
* button. The action button stays disabled while the text is blank or isn't
|
|
5
|
+
* valid, with the reason under the field. Enter means the action button, Esc
|
|
6
|
+
* and Cancel cancel, and the focus goes back to what opened it. The page
|
|
7
|
+
* needs no markup: the dialog (`#text-dialog`) is made the first time.
|
|
8
|
+
*
|
|
9
|
+
* @module ui/text-dialog
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { focusedElement } from '../dom/focus.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the dialog asks.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {object} TextRequest
|
|
18
|
+
* @property {string} title The dialog's title ("Rename folder").
|
|
19
|
+
* @property {string} label The field's label ("Name").
|
|
20
|
+
* @property {string} [value] The text to start with (selected). Default ''.
|
|
21
|
+
* @property {string} [action] The action button's text. Default "Save".
|
|
22
|
+
* @property {string} [cancel] The cancel button's text. Default "Cancel".
|
|
23
|
+
* @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
|
|
24
|
+
* shown under the field; null when it can. Default: anything but blank.
|
|
25
|
+
* @property {number} [maxLength] The field's `maxlength`. Default none.
|
|
26
|
+
* @property {HTMLElement | (() => HTMLElement | null | undefined) | null} [returnTo] Where the focus
|
|
27
|
+
* goes when it closes, or a function that says (asked then, so it can pick a row drawn since).
|
|
28
|
+
* Default what had the focus when it opened.
|
|
29
|
+
* @property {Document} [doc] The page. Default `document`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether a text can be used, and the reason to show when it can't. Pure.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} text - What's typed.
|
|
36
|
+
* @param {(text: string) => string | null} [validate] - Why a non-blank text can't be used, or null.
|
|
37
|
+
* @returns {{ok: boolean, reason: string}} `ok` when it can be used; `reason` is '' for blank text
|
|
38
|
+
* (nothing to say yet) and for a usable one.
|
|
39
|
+
* @example
|
|
40
|
+
* checkText(' '); // → {ok: false, reason: ''}
|
|
41
|
+
* checkText('x', () => 'Enter a site'); // → {ok: false, reason: 'Enter a site'}
|
|
42
|
+
*/
|
|
43
|
+
export function checkText(text, validate = () => null) {
|
|
44
|
+
if (!text.trim()) return { ok: false, reason: '' };
|
|
45
|
+
const reason = validate(text);
|
|
46
|
+
return { ok: reason === null, reason: reason ?? '' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The dialog's parts.
|
|
51
|
+
*
|
|
52
|
+
* @typedef {object} Parts
|
|
53
|
+
* @property {HTMLDialogElement} dialog The `<dialog>`.
|
|
54
|
+
* @property {HTMLFormElement} form Its form.
|
|
55
|
+
* @property {HTMLElement} title The title.
|
|
56
|
+
* @property {HTMLLabelElement} label The field's label.
|
|
57
|
+
* @property {HTMLInputElement} input The field.
|
|
58
|
+
* @property {HTMLElement} error The reason under the field.
|
|
59
|
+
* @property {HTMLButtonElement} cancel The cancel button.
|
|
60
|
+
* @property {HTMLButtonElement} confirm The action button.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** Each page's dialog, made on first use. @type {WeakMap<Document, Parts>} */
|
|
64
|
+
const made = new WeakMap();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The page's dialog, made the first time.
|
|
68
|
+
*
|
|
69
|
+
* @param {Document} doc - The page.
|
|
70
|
+
* @returns {Parts} Its parts.
|
|
71
|
+
*/
|
|
72
|
+
function partsFor(doc) {
|
|
73
|
+
const existing = made.get(doc);
|
|
74
|
+
if (existing) return existing;
|
|
75
|
+
/**
|
|
76
|
+
* @template {keyof HTMLElementTagNameMap} K
|
|
77
|
+
* @param {K} tag
|
|
78
|
+
* @param {Record<string, string>} [attributes]
|
|
79
|
+
* @returns {HTMLElementTagNameMap[K]}
|
|
80
|
+
*/
|
|
81
|
+
const el = (tag, attributes = {}) => {
|
|
82
|
+
const node = doc.createElement(tag);
|
|
83
|
+
for (const [name, value] of Object.entries(attributes)) node.setAttribute(name, value);
|
|
84
|
+
return node;
|
|
85
|
+
};
|
|
86
|
+
const dialog = el('dialog', { id: 'text-dialog', class: 'dialog', 'aria-labelledby': 'text-dialog-title' });
|
|
87
|
+
const form = el('form', { novalidate: '' });
|
|
88
|
+
const title = el('h2', { id: 'text-dialog-title', class: 'dialog-title' });
|
|
89
|
+
const body = el('div', { class: 'dialog-body' });
|
|
90
|
+
const field = el('div', { class: 'field' });
|
|
91
|
+
const label = el('label', { id: 'text-dialog-label', for: 'text-dialog-input' });
|
|
92
|
+
const input = el('input', { id: 'text-dialog-input', type: 'text', autocomplete: 'off', 'aria-describedby': 'text-dialog-error' });
|
|
93
|
+
const error = el('span', { id: 'text-dialog-error', class: 'hint error' });
|
|
94
|
+
error.hidden = true;
|
|
95
|
+
const buttons = el('div', { class: 'dialog-buttons' });
|
|
96
|
+
const cancel = el('button', { type: 'button', class: 'cancel' });
|
|
97
|
+
const confirm = el('button', { id: 'text-dialog-confirm', type: 'submit', class: 'action' });
|
|
98
|
+
field.append(label, input, error);
|
|
99
|
+
body.append(field);
|
|
100
|
+
buttons.append(cancel, confirm);
|
|
101
|
+
form.append(title, body, buttons);
|
|
102
|
+
dialog.append(form);
|
|
103
|
+
doc.body.append(dialog);
|
|
104
|
+
const parts = { dialog, form, title, label, input, error, cancel, confirm };
|
|
105
|
+
made.set(doc, parts);
|
|
106
|
+
return parts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The open dialog's settle function, per page. @type {WeakMap<Document, (text: string | null) => void>} */
|
|
110
|
+
const pending = new WeakMap();
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Ask for a piece of text in the page's one-field dialog.
|
|
114
|
+
*
|
|
115
|
+
* @param {TextRequest} request - What to ask.
|
|
116
|
+
* @returns {Promise<string | null>} The text, trimmed; null when cancelled.
|
|
117
|
+
* @example
|
|
118
|
+
* const name = await askText({title: 'Rename workspace', label: 'Name', value: workspace.name, maxLength: 200});
|
|
119
|
+
* if (name) rename(workspace, name);
|
|
120
|
+
*
|
|
121
|
+
* const site = await askText({
|
|
122
|
+
* title: 'Add site', label: 'Site', action: 'Add',
|
|
123
|
+
* validate: (text) => (siteFromInput(text) ? null : 'Enter a site, like example.com'),
|
|
124
|
+
* });
|
|
125
|
+
*/
|
|
126
|
+
export function askText({
|
|
127
|
+
title, label, value = '', action = 'Save', cancel = 'Cancel', validate, maxLength, returnTo, doc = document,
|
|
128
|
+
}) {
|
|
129
|
+
const parts = partsFor(doc);
|
|
130
|
+
pending.get(doc)?.(null);
|
|
131
|
+
const active = focusedElement(doc);
|
|
132
|
+
const back = returnTo === undefined ? (active instanceof /** @type {Window & typeof globalThis} */ (doc.defaultView).HTMLElement ? active : null) : returnTo;
|
|
133
|
+
|
|
134
|
+
parts.title.textContent = title;
|
|
135
|
+
parts.label.textContent = label;
|
|
136
|
+
parts.confirm.textContent = action;
|
|
137
|
+
parts.cancel.textContent = cancel;
|
|
138
|
+
parts.input.value = value;
|
|
139
|
+
if (maxLength === undefined) parts.input.removeAttribute('maxlength');
|
|
140
|
+
else parts.input.maxLength = maxLength;
|
|
141
|
+
|
|
142
|
+
const refresh = () => {
|
|
143
|
+
const { ok, reason } = checkText(parts.input.value, validate);
|
|
144
|
+
parts.error.textContent = reason;
|
|
145
|
+
parts.error.hidden = !reason;
|
|
146
|
+
parts.confirm.disabled = !ok;
|
|
147
|
+
};
|
|
148
|
+
refresh();
|
|
149
|
+
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const { signal } = controller;
|
|
153
|
+
/** @param {string | null} text */
|
|
154
|
+
const settle = (text) => {
|
|
155
|
+
controller.abort();
|
|
156
|
+
pending.delete(doc);
|
|
157
|
+
if (parts.dialog.open) parts.dialog.close();
|
|
158
|
+
resolve(text);
|
|
159
|
+
// After whatever the answer re-renders, unless another dialog opened meanwhile.
|
|
160
|
+
queueMicrotask(() => {
|
|
161
|
+
if (doc.querySelector('dialog[open]')) return;
|
|
162
|
+
const target = typeof back === 'function' ? back() : back;
|
|
163
|
+
if (target?.isConnected) target.focus();
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
pending.set(doc, settle);
|
|
167
|
+
parts.input.addEventListener('input', refresh, { signal });
|
|
168
|
+
// Enter submits through the action button (the form's only submit button), never while it's disabled.
|
|
169
|
+
parts.form.addEventListener('submit', (event) => {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
if (checkText(parts.input.value, validate).ok) settle(parts.input.value.trim());
|
|
172
|
+
}, { signal });
|
|
173
|
+
parts.cancel.addEventListener('click', () => settle(null), { signal });
|
|
174
|
+
parts.dialog.addEventListener('keydown', (event) => {
|
|
175
|
+
if (event.key !== 'Escape') return;
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
settle(null);
|
|
178
|
+
}, { signal });
|
|
179
|
+
parts.dialog.addEventListener('cancel', (event) => {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
settle(null);
|
|
182
|
+
}, { signal });
|
|
183
|
+
|
|
184
|
+
parts.dialog.showModal();
|
|
185
|
+
parts.input.select();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's toast (cr-toast), bottom left, as chrome://bookmarks shows one
|
|
3
|
+
* after a delete: "Deleted “Research” Undo". One per page, made by
|
|
4
|
+
* {@link createToast}; a new message replaces the last and restarts the wait.
|
|
5
|
+
*
|
|
6
|
+
* It's a manual `popover`, so it shows in the top layer: over anything the
|
|
7
|
+
* page stacks (a content script's toast over a site's own fixed bars), and
|
|
8
|
+
* never clipped by an ancestor.
|
|
9
|
+
*
|
|
10
|
+
* @module ui/toast
|
|
11
|
+
*/
|
|
12
|
+
/** How long chrome://bookmarks shows a toast (its cr-toast-manager's `duration`), in ms. */
|
|
13
|
+
export declare const TOAST_MS = 10000;
|
|
14
|
+
export type ToastOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* The undo button's text. Default "Undo".
|
|
17
|
+
*/
|
|
18
|
+
undoLabel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* How long a toast stays, in ms. Default {@link TOAST_MS}.
|
|
21
|
+
*/
|
|
22
|
+
duration?: number;
|
|
23
|
+
/**
|
|
24
|
+
* How long one with Undo stays. Default `duration`.
|
|
25
|
+
*/
|
|
26
|
+
undoDuration?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Where the toast goes: an element, or a shadow root (a content
|
|
29
|
+
* script's). Default the page's `body`.
|
|
30
|
+
*/
|
|
31
|
+
root?: ParentNode;
|
|
32
|
+
/**
|
|
33
|
+
* The page. Default `root`'s document, else `document`.
|
|
34
|
+
*/
|
|
35
|
+
doc?: Document;
|
|
36
|
+
};
|
|
37
|
+
export type Toast = {
|
|
38
|
+
/**
|
|
39
|
+
* The `#toast` element (`role="status"`, `popover="manual"`), in
|
|
40
|
+
* `root`; `:popover-open` while it shows.
|
|
41
|
+
*/
|
|
42
|
+
element: HTMLElement;
|
|
43
|
+
/**
|
|
44
|
+
* Show `text`, with an Undo
|
|
45
|
+
* button that hides the toast and runs `undo` when one is given.
|
|
46
|
+
*/
|
|
47
|
+
show: (text: string, undo?: (() => unknown) | null) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Take it down (and its Undo with it).
|
|
50
|
+
*/
|
|
51
|
+
hide: () => void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* What {@link createToast} takes.
|
|
55
|
+
*
|
|
56
|
+
* @typedef {object} ToastOptions
|
|
57
|
+
* @property {string} [undoLabel] The undo button's text. Default "Undo".
|
|
58
|
+
* @property {number} [duration] How long a toast stays, in ms. Default {@link TOAST_MS}.
|
|
59
|
+
* @property {number} [undoDuration] How long one with Undo stays. Default `duration`.
|
|
60
|
+
* @property {ParentNode} [root] Where the toast goes: an element, or a shadow root (a content
|
|
61
|
+
* script's). Default the page's `body`.
|
|
62
|
+
* @property {Document} [doc] The page. Default `root`'s document, else `document`.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* A page's toast.
|
|
66
|
+
*
|
|
67
|
+
* @typedef {object} Toast
|
|
68
|
+
* @property {HTMLElement} element The `#toast` element (`role="status"`, `popover="manual"`), in
|
|
69
|
+
* `root`; `:popover-open` while it shows.
|
|
70
|
+
* @property {(text: string, undo?: (() => unknown) | null) => void} show Show `text`, with an Undo
|
|
71
|
+
* button that hides the toast and runs `undo` when one is given.
|
|
72
|
+
* @property {() => void} hide Take it down (and its Undo with it).
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Make the page's toast: `<div id="toast" class="toast" role="status"
|
|
76
|
+
* popover="manual">` with `#toast-text` and an Undo button `#toast-undo`,
|
|
77
|
+
* closed until shown. Make it when the page loads, so screen readers know the
|
|
78
|
+
* live region before it speaks.
|
|
79
|
+
*
|
|
80
|
+
* @param {ToastOptions} [options] - The Undo text, how long it stays, and where it goes.
|
|
81
|
+
* @returns {Toast} The toast.
|
|
82
|
+
* @example
|
|
83
|
+
* const toast = createToast();
|
|
84
|
+
* await deleteWorkspace(workspace);
|
|
85
|
+
* toast.show(`Deleted “${workspace.name}”`, () => restore(workspace));
|
|
86
|
+
* // In a content script's shadow root:
|
|
87
|
+
* createToast({ root: host.attachShadow({ mode: 'open' }), duration: 4000 });
|
|
88
|
+
*/
|
|
89
|
+
export declare function createToast({ undoLabel, duration, undoDuration, root, doc }?: ToastOptions): Toast;
|
package/src/ui/toast.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's toast (cr-toast), bottom left, as chrome://bookmarks shows one
|
|
3
|
+
* after a delete: "Deleted “Research” Undo". One per page, made by
|
|
4
|
+
* {@link createToast}; a new message replaces the last and restarts the wait.
|
|
5
|
+
*
|
|
6
|
+
* It's a manual `popover`, so it shows in the top layer: over anything the
|
|
7
|
+
* page stacks (a content script's toast over a site's own fixed bars), and
|
|
8
|
+
* never clipped by an ancestor.
|
|
9
|
+
*
|
|
10
|
+
* @module ui/toast
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { rootDocument } from '../dom/focus.js';
|
|
14
|
+
|
|
15
|
+
/** How long chrome://bookmarks shows a toast (its cr-toast-manager's `duration`), in ms. */
|
|
16
|
+
export const TOAST_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What {@link createToast} takes.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {object} ToastOptions
|
|
22
|
+
* @property {string} [undoLabel] The undo button's text. Default "Undo".
|
|
23
|
+
* @property {number} [duration] How long a toast stays, in ms. Default {@link TOAST_MS}.
|
|
24
|
+
* @property {number} [undoDuration] How long one with Undo stays. Default `duration`.
|
|
25
|
+
* @property {ParentNode} [root] Where the toast goes: an element, or a shadow root (a content
|
|
26
|
+
* script's). Default the page's `body`.
|
|
27
|
+
* @property {Document} [doc] The page. Default `root`'s document, else `document`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A page's toast.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {object} Toast
|
|
34
|
+
* @property {HTMLElement} element The `#toast` element (`role="status"`, `popover="manual"`), in
|
|
35
|
+
* `root`; `:popover-open` while it shows.
|
|
36
|
+
* @property {(text: string, undo?: (() => unknown) | null) => void} show Show `text`, with an Undo
|
|
37
|
+
* button that hides the toast and runs `undo` when one is given.
|
|
38
|
+
* @property {() => void} hide Take it down (and its Undo with it).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Make the page's toast: `<div id="toast" class="toast" role="status"
|
|
43
|
+
* popover="manual">` with `#toast-text` and an Undo button `#toast-undo`,
|
|
44
|
+
* closed until shown. Make it when the page loads, so screen readers know the
|
|
45
|
+
* live region before it speaks.
|
|
46
|
+
*
|
|
47
|
+
* @param {ToastOptions} [options] - The Undo text, how long it stays, and where it goes.
|
|
48
|
+
* @returns {Toast} The toast.
|
|
49
|
+
* @example
|
|
50
|
+
* const toast = createToast();
|
|
51
|
+
* await deleteWorkspace(workspace);
|
|
52
|
+
* toast.show(`Deleted “${workspace.name}”`, () => restore(workspace));
|
|
53
|
+
* // In a content script's shadow root:
|
|
54
|
+
* createToast({ root: host.attachShadow({ mode: 'open' }), duration: 4000 });
|
|
55
|
+
*/
|
|
56
|
+
export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDuration = duration, root, doc = rootDocument(root) } = {}) {
|
|
57
|
+
const toast = doc.createElement('div');
|
|
58
|
+
toast.id = 'toast';
|
|
59
|
+
toast.className = 'toast';
|
|
60
|
+
toast.setAttribute('role', 'status');
|
|
61
|
+
toast.setAttribute('aria-live', 'polite');
|
|
62
|
+
toast.popover = 'manual';
|
|
63
|
+
const text = doc.createElement('span');
|
|
64
|
+
text.id = 'toast-text';
|
|
65
|
+
const undoButton = doc.createElement('button');
|
|
66
|
+
undoButton.id = 'toast-undo';
|
|
67
|
+
undoButton.type = 'button';
|
|
68
|
+
undoButton.textContent = undoLabel;
|
|
69
|
+
undoButton.hidden = true;
|
|
70
|
+
toast.append(text, undoButton);
|
|
71
|
+
(root ?? doc.body).append(toast);
|
|
72
|
+
|
|
73
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
74
|
+
let timer;
|
|
75
|
+
/** @type {(() => unknown) | null} */
|
|
76
|
+
let undoLast = null;
|
|
77
|
+
|
|
78
|
+
const hide = () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
toast.togglePopover(false);
|
|
81
|
+
undoLast = null;
|
|
82
|
+
};
|
|
83
|
+
undoButton.addEventListener('click', () => {
|
|
84
|
+
const undo = undoLast;
|
|
85
|
+
hide();
|
|
86
|
+
undo?.();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
element: toast,
|
|
91
|
+
show(message, undo = null) {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
text.textContent = message;
|
|
94
|
+
undoButton.hidden = !undo;
|
|
95
|
+
undoLast = undo;
|
|
96
|
+
// Hidden and shown again, so it's the newest in the top layer (over a popover since).
|
|
97
|
+
toast.togglePopover(false);
|
|
98
|
+
toast.togglePopover(true);
|
|
99
|
+
timer = setTimeout(hide, undo ? undoDuration : duration);
|
|
100
|
+
},
|
|
101
|
+
hide,
|
|
102
|
+
};
|
|
103
|
+
}
|