@arraypress/extension-kit 1.2.0 → 1.4.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 +147 -17
- 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/i18n.d.ts +11 -3
- package/src/dom/i18n.js +17 -4
- package/src/dom/setup.d.ts +55 -0
- package/src/dom/setup.js +47 -0
- package/src/index.d.ts +9 -1
- package/src/index.js +10 -1
- package/src/sites/sites.d.ts +114 -0
- package/src/sites/sites.js +169 -0
- package/src/styles/chrome-ui.css +13 -6
- 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 +7 -3
- package/src/testing/index.js +9 -3
- package/src/testing/pool.d.ts +23 -0
- package/src/testing/pool.js +33 -0
- package/src/time/time.d.ts +36 -0
- package/src/time/time.js +47 -0
- package/src/ui/menu.d.ts +13 -6
- package/src/ui/menu.js +11 -7
- package/src/ui/site-list.d.ts +216 -0
- package/src/ui/site-list.js +234 -0
- package/src/ui/text-dialog.js +3 -1
- package/src/ui/toast.d.ts +25 -9
- package/src/ui/toast.js +25 -11
package/src/testing/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The kit's test helpers: a fake `chrome.storage`, Playwright helpers for
|
|
3
|
-
* end-to-end tests
|
|
4
|
-
* and
|
|
3
|
+
* end-to-end tests (time limits, a pool, a browser that passes for a
|
|
4
|
+
* person's) and a server for their pages, Chrome's install warnings and
|
|
5
|
+
* saved tab groups, and checks for a packaging test. Node only.
|
|
5
6
|
*
|
|
6
7
|
* @module testing
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
export {
|
|
10
|
+
export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
|
|
11
|
+
export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
|
|
12
|
+
export {
|
|
13
|
+
CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
|
|
14
|
+
} from './e2e.js';
|
|
15
|
+
export { eachLimited } from './pool.js';
|
|
10
16
|
export { serve } from './server.js';
|
|
11
17
|
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
12
18
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working through a list a few at a time: a live check of many sites, each
|
|
3
|
+
* in one of a few browsers.
|
|
4
|
+
*
|
|
5
|
+
* @module testing/pool
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Run `work` on every item, at most `limit` at once, each slot taking the
|
|
9
|
+
* next item as it finishes one. Errors are `work`'s to handle: one that
|
|
10
|
+
* throws stops its slot, and the returned promise rejects once the others
|
|
11
|
+
* are done.
|
|
12
|
+
*
|
|
13
|
+
* @template T
|
|
14
|
+
* @param {readonly T[]} items - The items.
|
|
15
|
+
* @param {number} limit - At most this many at once.
|
|
16
|
+
* @param {(item: T, slot: number, index: number) => Promise<void>} work - Deals with one item;
|
|
17
|
+
* `slot` (0 to limit − 1) is the same for everything one slot does, for per-slot resources
|
|
18
|
+
* (a browser each).
|
|
19
|
+
* @returns {Promise<void>} Once every item is done.
|
|
20
|
+
* @example
|
|
21
|
+
* await eachLimited(sites, 3, async (site, slot) => check(site, browsers[slot]));
|
|
22
|
+
*/
|
|
23
|
+
export declare function eachLimited<T>(items: readonly T[], limit: number, work: (item: T, slot: number, index: number) => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working through a list a few at a time: a live check of many sites, each
|
|
3
|
+
* in one of a few browsers.
|
|
4
|
+
*
|
|
5
|
+
* @module testing/pool
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run `work` on every item, at most `limit` at once, each slot taking the
|
|
10
|
+
* next item as it finishes one. Errors are `work`'s to handle: one that
|
|
11
|
+
* throws stops its slot, and the returned promise rejects once the others
|
|
12
|
+
* are done.
|
|
13
|
+
*
|
|
14
|
+
* @template T
|
|
15
|
+
* @param {readonly T[]} items - The items.
|
|
16
|
+
* @param {number} limit - At most this many at once.
|
|
17
|
+
* @param {(item: T, slot: number, index: number) => Promise<void>} work - Deals with one item;
|
|
18
|
+
* `slot` (0 to limit − 1) is the same for everything one slot does, for per-slot resources
|
|
19
|
+
* (a browser each).
|
|
20
|
+
* @returns {Promise<void>} Once every item is done.
|
|
21
|
+
* @example
|
|
22
|
+
* await eachLimited(sites, 3, async (site, slot) => check(site, browsers[slot]));
|
|
23
|
+
*/
|
|
24
|
+
export async function eachLimited(items, limit, work) {
|
|
25
|
+
let next = 0;
|
|
26
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async (_, slot) => {
|
|
27
|
+
while (next < items.length) {
|
|
28
|
+
const index = next;
|
|
29
|
+
next += 1;
|
|
30
|
+
await work(items[index], slot, index);
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time limits for work that may never finish: a Chrome call a service worker
|
|
3
|
+
* shouldn't wait on forever, or, in tests that drive a real browser, a page
|
|
4
|
+
* that stops responding. Runs anywhere (worker, page, Node).
|
|
5
|
+
*
|
|
6
|
+
* @module time/time
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Settle `promise` within `ms`, or settle the way `late` says. The timer is
|
|
10
|
+
* cleared as soon as either happens, so a finished run isn't kept waiting
|
|
11
|
+
* for it. The work itself isn't stopped.
|
|
12
|
+
*
|
|
13
|
+
* @template T
|
|
14
|
+
* @param {Promise<T>} promise - The work.
|
|
15
|
+
* @param {number} ms - The limit.
|
|
16
|
+
* @param {(resolve: (value: T) => void, reject: (error: Error) => void) => void} late - Runs when the
|
|
17
|
+
* limit passes first.
|
|
18
|
+
* @returns {Promise<T>} The work's outcome, or `late`'s.
|
|
19
|
+
*/
|
|
20
|
+
export declare function deadline<T>(promise: Promise<T>, ms: number, late: (resolve: (value: T) => void, reject: (error: Error) => void) => void): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* A promise's value within a time limit, or `fallback` when it's late or
|
|
23
|
+
* fails: for a step to give up on and go past (a Chrome call that never
|
|
24
|
+
* answers, a renderer that stopped responding, a close that never returns).
|
|
25
|
+
* The work itself isn't stopped; the timer is cleared once it settles.
|
|
26
|
+
*
|
|
27
|
+
* @template T
|
|
28
|
+
* @param {Promise<T>} promise - The work.
|
|
29
|
+
* @param {number} ms - The limit.
|
|
30
|
+
* @param {T} fallback - What to give if it's late or fails.
|
|
31
|
+
* @returns {Promise<T>} Its value, or the fallback. Never rejects.
|
|
32
|
+
* @example
|
|
33
|
+
* const list = await within(handled(worker, page), 8000, []);
|
|
34
|
+
* await within(page.close(), 15000, undefined);
|
|
35
|
+
*/
|
|
36
|
+
export declare function within<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T>;
|
package/src/time/time.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time limits for work that may never finish: a Chrome call a service worker
|
|
3
|
+
* shouldn't wait on forever, or, in tests that drive a real browser, a page
|
|
4
|
+
* that stops responding. Runs anywhere (worker, page, Node).
|
|
5
|
+
*
|
|
6
|
+
* @module time/time
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Settle `promise` within `ms`, or settle the way `late` says. The timer is
|
|
11
|
+
* cleared as soon as either happens, so a finished run isn't kept waiting
|
|
12
|
+
* for it. The work itself isn't stopped.
|
|
13
|
+
*
|
|
14
|
+
* @template T
|
|
15
|
+
* @param {Promise<T>} promise - The work.
|
|
16
|
+
* @param {number} ms - The limit.
|
|
17
|
+
* @param {(resolve: (value: T) => void, reject: (error: Error) => void) => void} late - Runs when the
|
|
18
|
+
* limit passes first.
|
|
19
|
+
* @returns {Promise<T>} The work's outcome, or `late`'s.
|
|
20
|
+
*/
|
|
21
|
+
export function deadline(promise, ms, late) {
|
|
22
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
23
|
+
let timer;
|
|
24
|
+
const limit = new Promise((resolve, reject) => {
|
|
25
|
+
timer = setTimeout(() => late(resolve, reject), ms);
|
|
26
|
+
});
|
|
27
|
+
return /** @type {Promise<T>} */ (Promise.race([promise, limit])).finally(() => clearTimeout(timer));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A promise's value within a time limit, or `fallback` when it's late or
|
|
32
|
+
* fails: for a step to give up on and go past (a Chrome call that never
|
|
33
|
+
* answers, a renderer that stopped responding, a close that never returns).
|
|
34
|
+
* The work itself isn't stopped; the timer is cleared once it settles.
|
|
35
|
+
*
|
|
36
|
+
* @template T
|
|
37
|
+
* @param {Promise<T>} promise - The work.
|
|
38
|
+
* @param {number} ms - The limit.
|
|
39
|
+
* @param {T} fallback - What to give if it's late or fails.
|
|
40
|
+
* @returns {Promise<T>} Its value, or the fallback. Never rejects.
|
|
41
|
+
* @example
|
|
42
|
+
* const list = await within(handled(worker, page), 8000, []);
|
|
43
|
+
* await within(page.close(), 15000, undefined);
|
|
44
|
+
*/
|
|
45
|
+
export function within(promise, ms, fallback) {
|
|
46
|
+
return deadline(promise.catch(() => fallback), ms, (resolve) => resolve(fallback));
|
|
47
|
+
}
|
package/src/ui/menu.d.ts
CHANGED
|
@@ -140,13 +140,18 @@ export type MenuOptions = {
|
|
|
140
140
|
*/
|
|
141
141
|
placement?: 'over' | 'below';
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
143
|
+
* Where the menu goes: an element, or a shadow root (a content
|
|
144
|
+
* script's, so the page's styles never reach it). Default the page's `body`.
|
|
145
|
+
*/
|
|
146
|
+
root?: ParentNode;
|
|
147
|
+
/**
|
|
148
|
+
* The page. Default `root`'s document, else `document`.
|
|
144
149
|
*/
|
|
145
150
|
doc?: Document;
|
|
146
151
|
};
|
|
147
152
|
export type ActionMenu = {
|
|
148
153
|
/**
|
|
149
|
-
* The `<dialog class="menu" role="menu">`, in
|
|
154
|
+
* The `<dialog class="menu" role="menu">`, in `root`.
|
|
150
155
|
*/
|
|
151
156
|
element: HTMLDialogElement;
|
|
152
157
|
/**
|
|
@@ -165,13 +170,15 @@ export type ActionMenu = {
|
|
|
165
170
|
* @typedef {object} MenuOptions
|
|
166
171
|
* @property {string} [id] The menu element's id.
|
|
167
172
|
* @property {'over' | 'below'} [placement] Where it opens from a button. Default `'over'`, as Chrome's.
|
|
168
|
-
* @property {
|
|
173
|
+
* @property {ParentNode} [root] Where the menu goes: an element, or a shadow root (a content
|
|
174
|
+
* script's, so the page's styles never reach it). Default the page's `body`.
|
|
175
|
+
* @property {Document} [doc] The page. Default `root`'s document, else `document`.
|
|
169
176
|
*/
|
|
170
177
|
/**
|
|
171
178
|
* A page's action menu.
|
|
172
179
|
*
|
|
173
180
|
* @typedef {object} ActionMenu
|
|
174
|
-
* @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in
|
|
181
|
+
* @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in `root`.
|
|
175
182
|
* @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
|
|
176
183
|
* and open it (closing it first if it's open).
|
|
177
184
|
* @property {() => void} close Close it, if open, and return the focus.
|
|
@@ -180,7 +187,7 @@ export type ActionMenu = {
|
|
|
180
187
|
* Make a page's action menu: one `<dialog class="menu">` that any number of
|
|
181
188
|
* ⋮ buttons (or right-clicks) open with their own items.
|
|
182
189
|
*
|
|
183
|
-
* @param {MenuOptions} [options] - Its id, placement and
|
|
190
|
+
* @param {MenuOptions} [options] - Its id, placement, and where it goes.
|
|
184
191
|
* @returns {ActionMenu} The menu.
|
|
185
192
|
* @example
|
|
186
193
|
* const menu = createMenu();
|
|
@@ -192,7 +199,7 @@ export type ActionMenu = {
|
|
|
192
199
|
* ], {anchor: button}),
|
|
193
200
|
* }));
|
|
194
201
|
*/
|
|
195
|
-
export declare function createMenu({ id, placement, doc }?: MenuOptions): ActionMenu;
|
|
202
|
+
export declare function createMenu({ id, placement, root, doc }?: MenuOptions): ActionMenu;
|
|
196
203
|
export type MoreActionsOptions = {
|
|
197
204
|
/**
|
|
198
205
|
* The whole label, for a page that translates it. Default
|
package/src/ui/menu.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* @module ui/menu
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { focusedElement, rootDocument } from '../dom/focus.js';
|
|
18
|
+
|
|
17
19
|
/** The gap kept between a menu and the window's edges, in px. */
|
|
18
20
|
export const MENU_EDGE = 8;
|
|
19
21
|
|
|
@@ -103,14 +105,16 @@ export function isInside(box, x, y) {
|
|
|
103
105
|
* @typedef {object} MenuOptions
|
|
104
106
|
* @property {string} [id] The menu element's id.
|
|
105
107
|
* @property {'over' | 'below'} [placement] Where it opens from a button. Default `'over'`, as Chrome's.
|
|
106
|
-
* @property {
|
|
108
|
+
* @property {ParentNode} [root] Where the menu goes: an element, or a shadow root (a content
|
|
109
|
+
* script's, so the page's styles never reach it). Default the page's `body`.
|
|
110
|
+
* @property {Document} [doc] The page. Default `root`'s document, else `document`.
|
|
107
111
|
*/
|
|
108
112
|
|
|
109
113
|
/**
|
|
110
114
|
* A page's action menu.
|
|
111
115
|
*
|
|
112
116
|
* @typedef {object} ActionMenu
|
|
113
|
-
* @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in
|
|
117
|
+
* @property {HTMLDialogElement} element The `<dialog class="menu" role="menu">`, in `root`.
|
|
114
118
|
* @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
|
|
115
119
|
* and open it (closing it first if it's open).
|
|
116
120
|
* @property {() => void} close Close it, if open, and return the focus.
|
|
@@ -120,7 +124,7 @@ export function isInside(box, x, y) {
|
|
|
120
124
|
* Make a page's action menu: one `<dialog class="menu">` that any number of
|
|
121
125
|
* ⋮ buttons (or right-clicks) open with their own items.
|
|
122
126
|
*
|
|
123
|
-
* @param {MenuOptions} [options] - Its id, placement and
|
|
127
|
+
* @param {MenuOptions} [options] - Its id, placement, and where it goes.
|
|
124
128
|
* @returns {ActionMenu} The menu.
|
|
125
129
|
* @example
|
|
126
130
|
* const menu = createMenu();
|
|
@@ -132,13 +136,13 @@ export function isInside(box, x, y) {
|
|
|
132
136
|
* ], {anchor: button}),
|
|
133
137
|
* }));
|
|
134
138
|
*/
|
|
135
|
-
export function createMenu({ id, placement = 'over', doc =
|
|
139
|
+
export function createMenu({ id, placement = 'over', root, doc = rootDocument(root) } = {}) {
|
|
136
140
|
const view = /** @type {Window & typeof globalThis} */ (doc.defaultView);
|
|
137
141
|
const menu = doc.createElement('dialog');
|
|
138
142
|
menu.className = 'menu';
|
|
139
143
|
menu.setAttribute('role', 'menu');
|
|
140
144
|
if (id) menu.id = id;
|
|
141
|
-
doc.body.append(menu);
|
|
145
|
+
(root ?? doc.body).append(menu);
|
|
142
146
|
|
|
143
147
|
/** @type {HTMLElement | null} */
|
|
144
148
|
let returnFocus = null;
|
|
@@ -164,7 +168,7 @@ export function createMenu({ id, placement = 'over', doc = document } = {}) {
|
|
|
164
168
|
|
|
165
169
|
menu.addEventListener('keydown', (event) => {
|
|
166
170
|
const list = items();
|
|
167
|
-
const at = list.indexOf(/** @type {HTMLButtonElement} */ (doc
|
|
171
|
+
const at = list.indexOf(/** @type {HTMLButtonElement} */ (focusedElement(doc)));
|
|
168
172
|
/** @param {number} index */
|
|
169
173
|
const go = (index) => {
|
|
170
174
|
event.preventDefault();
|
|
@@ -215,7 +219,7 @@ export function createMenu({ id, placement = 'over', doc = document } = {}) {
|
|
|
215
219
|
if (label) menu.setAttribute('aria-label', label);
|
|
216
220
|
else menu.removeAttribute('aria-label');
|
|
217
221
|
|
|
218
|
-
const active = doc
|
|
222
|
+
const active = focusedElement(doc);
|
|
219
223
|
returnFocus = focusTo ?? anchor ?? (active instanceof view.HTMLElement ? active : null);
|
|
220
224
|
if (anchor?.hasAttribute('aria-haspopup')) {
|
|
221
225
|
anchor.setAttribute('aria-expanded', 'true');
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settings card's list of sites (“Never on”, “Allowed sites”), in one of
|
|
3
|
+
* Chrome's two ways:
|
|
4
|
+
*
|
|
5
|
+
* - **With an add row** (`form`), as a short list in a chrome://settings
|
|
6
|
+
* card: a row per site with a Remove button, and below them a field and an
|
|
7
|
+
* Add button.
|
|
8
|
+
* - **With an Add button and a dialog** (`addButton`), as
|
|
9
|
+
* chrome://settings/performance lists the sites Memory Saver keeps active:
|
|
10
|
+
* the button asks for a site in the one-field dialog (“Add site”), and each
|
|
11
|
+
* row has a ⋮ menu with Edit and Remove.
|
|
12
|
+
*
|
|
13
|
+
* Either way, an optional “No sites added” row shows while there are none.
|
|
14
|
+
* The page's markup holds the list, the empty row and the form or button (so
|
|
15
|
+
* they're localised with the rest of the page); this fills the list and wires
|
|
16
|
+
* them up.
|
|
17
|
+
*
|
|
18
|
+
* ```html
|
|
19
|
+
* <ul class="list" id="never-sites"></ul>
|
|
20
|
+
* <p class="row text" id="no-sites"><span class="label secondary">No sites added</span></p>
|
|
21
|
+
* <form class="row add" id="add-never">
|
|
22
|
+
* <input type="text" placeholder="example.com" aria-label="Site" autocomplete="off" spellcheck="false">
|
|
23
|
+
* <button type="submit">Add</button>
|
|
24
|
+
* </form>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module ui/site-list
|
|
28
|
+
*/
|
|
29
|
+
export type SiteListWords = {
|
|
30
|
+
/**
|
|
31
|
+
* The Add button in the dialog.
|
|
32
|
+
*/
|
|
33
|
+
add: string;
|
|
34
|
+
/**
|
|
35
|
+
* The dialog's title when adding.
|
|
36
|
+
*/
|
|
37
|
+
addSite: string;
|
|
38
|
+
/**
|
|
39
|
+
* The dialog's title when editing.
|
|
40
|
+
*/
|
|
41
|
+
editSite: string;
|
|
42
|
+
/**
|
|
43
|
+
* The ⋮ menu's Edit item.
|
|
44
|
+
*/
|
|
45
|
+
edit: string;
|
|
46
|
+
/**
|
|
47
|
+
* The Remove button, or the ⋮ menu's Remove item.
|
|
48
|
+
*/
|
|
49
|
+
remove: string;
|
|
50
|
+
/**
|
|
51
|
+
* The dialog's action when editing.
|
|
52
|
+
*/
|
|
53
|
+
save: string;
|
|
54
|
+
/**
|
|
55
|
+
* The dialog's Cancel button.
|
|
56
|
+
*/
|
|
57
|
+
cancel: string;
|
|
58
|
+
/**
|
|
59
|
+
* The dialog's field label.
|
|
60
|
+
*/
|
|
61
|
+
site: string;
|
|
62
|
+
/**
|
|
63
|
+
* Under the dialog's field while what's typed isn't a site.
|
|
64
|
+
*/
|
|
65
|
+
invalid: string;
|
|
66
|
+
/**
|
|
67
|
+
* The Remove button's name for screen readers.
|
|
68
|
+
*/
|
|
69
|
+
removeLabel: (label: string) => string;
|
|
70
|
+
/**
|
|
71
|
+
* The ⋮ button's name for screen readers.
|
|
72
|
+
*/
|
|
73
|
+
moreActions: (label: string) => string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The list's words, Chrome's English by default; a page passes its translations.
|
|
77
|
+
*
|
|
78
|
+
* @typedef {object} SiteListWords
|
|
79
|
+
* @property {string} add The Add button in the dialog.
|
|
80
|
+
* @property {string} addSite The dialog's title when adding.
|
|
81
|
+
* @property {string} editSite The dialog's title when editing.
|
|
82
|
+
* @property {string} edit The ⋮ menu's Edit item.
|
|
83
|
+
* @property {string} remove The Remove button, or the ⋮ menu's Remove item.
|
|
84
|
+
* @property {string} save The dialog's action when editing.
|
|
85
|
+
* @property {string} cancel The dialog's Cancel button.
|
|
86
|
+
* @property {string} site The dialog's field label.
|
|
87
|
+
* @property {string} invalid Under the dialog's field while what's typed isn't a site.
|
|
88
|
+
* @property {(label: string) => string} removeLabel The Remove button's name for screen readers.
|
|
89
|
+
* @property {(label: string) => string} moreActions The ⋮ button's name for screen readers.
|
|
90
|
+
*/
|
|
91
|
+
/** @type {Readonly<SiteListWords>} */
|
|
92
|
+
export declare const SITE_LIST_WORDS: Readonly<SiteListWords>;
|
|
93
|
+
/**
|
|
94
|
+
* The message key each word is read from when a page passes its `t`, so
|
|
95
|
+
* every extension's `_locales` names them alike. `removeLabel` and
|
|
96
|
+
* `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
|
|
97
|
+
*
|
|
98
|
+
* @type {Readonly<Record<keyof SiteListWords, string>>}
|
|
99
|
+
*/
|
|
100
|
+
export declare const SITE_LIST_MESSAGES: Readonly<Record<keyof SiteListWords, string>>;
|
|
101
|
+
export type SiteListOptions = {
|
|
102
|
+
/**
|
|
103
|
+
* The `<ul class="list">` to fill.
|
|
104
|
+
*/
|
|
105
|
+
list: HTMLElement;
|
|
106
|
+
/**
|
|
107
|
+
* Shown while the list is empty.
|
|
108
|
+
*/
|
|
109
|
+
empty?: HTMLElement;
|
|
110
|
+
/**
|
|
111
|
+
* An add row: a form with one text field and a submit button.
|
|
112
|
+
*/
|
|
113
|
+
form?: HTMLFormElement;
|
|
114
|
+
/**
|
|
115
|
+
* Or an Add button that asks in the dialog; each row then has a
|
|
116
|
+
* ⋮ menu (Edit, Remove) instead of a Remove button.
|
|
117
|
+
*/
|
|
118
|
+
addButton?: HTMLElement;
|
|
119
|
+
/**
|
|
120
|
+
* What was typed as the site the list keeps
|
|
121
|
+
* (`siteHost`, `siteOrigin`), or null when it isn't one.
|
|
122
|
+
*/
|
|
123
|
+
parse: (text: string) => string | null;
|
|
124
|
+
/**
|
|
125
|
+
* Adds a parsed site (asking Chrome for access first, if
|
|
126
|
+
* the list needs it). Resolve to false to keep the text in the add row's field (access refused).
|
|
127
|
+
*/
|
|
128
|
+
add: (site: string) => unknown;
|
|
129
|
+
/**
|
|
130
|
+
* Removes a site.
|
|
131
|
+
*/
|
|
132
|
+
remove: (site: string) => unknown;
|
|
133
|
+
/**
|
|
134
|
+
* Puts an edited site in place of
|
|
135
|
+
* the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
|
|
136
|
+
*/
|
|
137
|
+
replace?: (site: string, edited: string) => unknown;
|
|
138
|
+
/**
|
|
139
|
+
* The add row: called with what was typed when it isn't a site.
|
|
140
|
+
*/
|
|
141
|
+
invalid?: (text: string) => void;
|
|
142
|
+
/**
|
|
143
|
+
* How a site reads. Default {@link siteLabel}.
|
|
144
|
+
*/
|
|
145
|
+
label?: (site: string) => string;
|
|
146
|
+
/**
|
|
147
|
+
* The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
148
|
+
*/
|
|
149
|
+
maxLength?: number;
|
|
150
|
+
/**
|
|
151
|
+
* The page's message lookup
|
|
152
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
153
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
154
|
+
*/
|
|
155
|
+
t?: (key: string, substitutions?: string | string[]) => string;
|
|
156
|
+
/**
|
|
157
|
+
* Words that differ from those, by name.
|
|
158
|
+
*/
|
|
159
|
+
words?: Partial<SiteListWords>;
|
|
160
|
+
};
|
|
161
|
+
export type SiteList = {
|
|
162
|
+
/**
|
|
163
|
+
* Show these sites (call it whenever they change).
|
|
164
|
+
*/
|
|
165
|
+
render: (sites: readonly string[]) => void;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* What {@link createSiteList} takes: the list, one way to add to it, and
|
|
169
|
+
* what adding, removing and editing do.
|
|
170
|
+
*
|
|
171
|
+
* @typedef {object} SiteListOptions
|
|
172
|
+
* @property {HTMLElement} list The `<ul class="list">` to fill.
|
|
173
|
+
* @property {HTMLElement} [empty] Shown while the list is empty.
|
|
174
|
+
* @property {HTMLFormElement} [form] An add row: a form with one text field and a submit button.
|
|
175
|
+
* @property {HTMLElement} [addButton] Or an Add button that asks in the dialog; each row then has a
|
|
176
|
+
* ⋮ menu (Edit, Remove) instead of a Remove button.
|
|
177
|
+
* @property {(text: string) => string | null} parse What was typed as the site the list keeps
|
|
178
|
+
* (`siteHost`, `siteOrigin`), or null when it isn't one.
|
|
179
|
+
* @property {(site: string) => unknown} add Adds a parsed site (asking Chrome for access first, if
|
|
180
|
+
* the list needs it). Resolve to false to keep the text in the add row's field (access refused).
|
|
181
|
+
* @property {(site: string) => unknown} remove Removes a site.
|
|
182
|
+
* @property {(site: string, edited: string) => unknown} [replace] Puts an edited site in place of
|
|
183
|
+
* the old one (the ⋮ menu's Edit; without it, the menu has only Remove).
|
|
184
|
+
* @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
|
|
185
|
+
* @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
|
|
186
|
+
* @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
187
|
+
* @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
|
|
188
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
189
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
190
|
+
* @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* A site list.
|
|
194
|
+
*
|
|
195
|
+
* @typedef {object} SiteList
|
|
196
|
+
* @property {(sites: readonly string[]) => void} render Show these sites (call it whenever they change).
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* Fill a site list and wire its way of adding: an add row (typing a site and
|
|
200
|
+
* pressing Enter or Add) or an Add button (the dialog, which keeps its Add off
|
|
201
|
+
* until what's typed is a site). A row's Remove (or its ⋮ menu's Remove and
|
|
202
|
+
* Edit) hands its site to `remove` (or `replace`).
|
|
203
|
+
*
|
|
204
|
+
* @param {SiteListOptions} options - The markup, the parser and what adding and removing do.
|
|
205
|
+
* @returns {SiteList} The list.
|
|
206
|
+
* @example
|
|
207
|
+
* const never = createSiteList({
|
|
208
|
+
* list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
|
|
209
|
+
* parse: siteHost,
|
|
210
|
+
* add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
|
|
211
|
+
* remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
|
|
212
|
+
* t, // its words from optionsAdd, optionsRemove, …
|
|
213
|
+
* });
|
|
214
|
+
* never.render(settings.neverSites);
|
|
215
|
+
*/
|
|
216
|
+
export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, t, words, }: SiteListOptions): SiteList;
|