@arraypress/extension-kit 1.3.0 → 1.5.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 +86 -19
- package/package.json +1 -1
- package/src/build/vendor.js +1 -1
- package/src/dom/i18n.d.ts +11 -3
- package/src/dom/i18n.js +17 -4
- package/src/dom/setup.d.ts +4 -3
- package/src/dom/setup.js +3 -2
- package/src/index.d.ts +8 -3
- package/src/index.js +8 -3
- package/src/sites/sites.d.ts +33 -1
- package/src/sites/sites.js +42 -1
- package/src/testing/browser.js +1 -1
- package/src/testing/e2e.js +1 -1
- package/src/testing/index.d.ts +0 -1
- package/src/testing/index.js +0 -1
- package/src/{testing → time}/time.d.ts +7 -6
- package/src/{testing → time}/time.js +7 -6
- package/src/ui/site-list.d.ts +23 -6
- package/src/ui/site-list.js +46 -6
- package/src/ui/text-dialog.d.ts +107 -37
- package/src/ui/text-dialog.js +167 -72
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
|
|
|
10
10
|
service worker shows the matching icon.
|
|
11
11
|
- **About links**: README, Issues and PRIVACY.md in the extension's GitHub repository, plus
|
|
12
12
|
package-time warnings for placeholder links.
|
|
13
|
-
- **Chrome's page components**: the ⋮ action menu and its row button, the
|
|
14
|
-
("Rename folder"), the toast with Undo and a settings card's site list, built on the
|
|
13
|
+
- **Chrome's page components**: the ⋮ action menu and its row button, the text dialog
|
|
14
|
+
("Rename folder", or several fields as "Add site search"), the toast with Undo and a settings card's site list, built on the
|
|
15
15
|
stylesheet's classes; they work in a content script's shadow root too.
|
|
16
16
|
- **Sites**: what people type into a per-site list, and whether a page is on one.
|
|
17
17
|
- **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
|
|
@@ -159,13 +159,25 @@ be public before the store listing goes live.
|
|
|
159
159
|
## Page start-up
|
|
160
160
|
|
|
161
161
|
```js
|
|
162
|
-
const t = setUpPage(chrome, { title: 'optionsTitle'
|
|
162
|
+
const t = setUpPage(chrome, { title: 'optionsTitle' });
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
Reports the page's colour scheme for the toolbar glyph (`watchColorScheme`; a failure is only
|
|
166
166
|
logged), fills its `data-i18n` text (`localize`), sets its title from a message, and returns the
|
|
167
167
|
page's `t`.
|
|
168
168
|
|
|
169
|
+
The extension's name lives in one message, `extShortName`. A message that names the extension
|
|
170
|
+
takes it as a placeholder whose content is `$1`, and every lookup without substitutions (`t(key)`,
|
|
171
|
+
`data-i18n`, `data-i18n-*`, the title) fills it in:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
"optionsAbout": { "message": "About $NAME$", "placeholders": { "name": { "content": "$1" } } }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<h2 class="section-title" data-i18n="optionsAbout"></h2> <!-- About Upright -->
|
|
179
|
+
```
|
|
180
|
+
|
|
169
181
|
## Sites
|
|
170
182
|
|
|
171
183
|
```js
|
|
@@ -175,11 +187,22 @@ onSite(siteHost(location.href), neverSites); // on a listed site or a subdomain
|
|
|
175
187
|
hostPatterns('github.com'); // ['*://github.com/*', '*://*.github.com/*'] (excludeMatches)
|
|
176
188
|
originPattern('https://bit.ly/x'); // 'https://bit.ly/*' (permissions.request)
|
|
177
189
|
siteLabel('https://example.com'); // 'example.com', as a list shows it
|
|
190
|
+
withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list keeps after Add (also withoutSite, withSiteReplaced)
|
|
178
191
|
```
|
|
179
192
|
|
|
180
193
|
A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
|
|
181
194
|
`localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
|
|
182
195
|
|
|
196
|
+
## Time limits
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const groups = await within(chrome.tabGroups.query({}), 2000, []); // a Chrome call that may never answer
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`within(promise, ms, fallback)`: the value in time, or the fallback (late or failed); never
|
|
203
|
+
rejects; its timer cleared once the work settles (the work itself isn't stopped). In the runtime
|
|
204
|
+
half, so a service worker or page can use it; the test helpers use it too.
|
|
205
|
+
|
|
183
206
|
## Page components
|
|
184
207
|
|
|
185
208
|
Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
|
|
@@ -215,7 +238,7 @@ opens it below the button instead. `menuPosition` and `isInside` are the pure ge
|
|
|
215
238
|
`tooltip` (a `title`) and `focusable: false` (out of the tab order, for a row that takes the focus)
|
|
216
239
|
cover the rest.
|
|
217
240
|
|
|
218
|
-
###
|
|
241
|
+
### Text dialog (cr-dialog)
|
|
219
242
|
|
|
220
243
|
```js
|
|
221
244
|
const name = await askText({ title: 'Rename workspace', label: 'Name', value: workspace.name, maxLength: 200 });
|
|
@@ -231,6 +254,26 @@ disabled while the text is blank or `validate` gives a reason, which shows under
|
|
|
231
254
|
focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
|
|
232
255
|
One `#text-dialog` per page, made on first use.
|
|
233
256
|
|
|
257
|
+
### Dialog with several fields
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
const rule = await askFields({
|
|
261
|
+
title: 'Add folder rule', action: 'Add',
|
|
262
|
+
fields: [
|
|
263
|
+
{ name: 'from', label: 'Downloads from', placeholder: 'example.com or .pdf', validate: checkFrom },
|
|
264
|
+
{ name: 'folder', label: 'Folder', value: 'Receipts' },
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
if (rule) addRule(rule.from, rule.folder);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The same dialog as chrome://settings/searchEngines asks for a site search: a field each, in
|
|
271
|
+
order. Resolves to each field's text by its `name`, trimmed, or `null` when cancelled. The action
|
|
272
|
+
button stays disabled until every field is filled in and valid, each field's reason under it; the
|
|
273
|
+
focus starts in the first empty field. `askText` is this with one field. A one-field dialog's ids
|
|
274
|
+
are `text-dialog-input` and `text-dialog-error`; with several, `text-dialog-<name>` and
|
|
275
|
+
`text-dialog-<name>-error` (`fieldIds`), for a test to find them.
|
|
276
|
+
|
|
234
277
|
### Toast with Undo (cr-toast)
|
|
235
278
|
|
|
236
279
|
```js
|
|
@@ -247,25 +290,50 @@ restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duratio
|
|
|
247
290
|
### Site list
|
|
248
291
|
|
|
249
292
|
```js
|
|
250
|
-
//
|
|
251
|
-
// <
|
|
252
|
-
// <
|
|
293
|
+
// Memory Saver's way (chrome://settings/performance): an Add button and the dialog, ⋮ Edit/Remove.
|
|
294
|
+
// <div class="row"><span class="label secondary">…</span><button id="add-site" type="button">Add</button></div>
|
|
295
|
+
// <p class="row text" id="no-sites"><span class="label">No sites added</span></p> <ul class="list" id="sites"></ul>
|
|
253
296
|
const sites = createSiteList({
|
|
254
|
-
list: byId('sites'), empty: byId('no-sites'),
|
|
255
|
-
parse:
|
|
256
|
-
add:
|
|
257
|
-
remove: (
|
|
258
|
-
|
|
297
|
+
list: byId('sites'), empty: byId('no-sites'), addButton: byId('add-site'),
|
|
298
|
+
parse: siteHost,
|
|
299
|
+
add: (site) => save({ sites: withSite(settings.sites, site) }),
|
|
300
|
+
remove: (site) => save({ sites: withoutSite(settings.sites, site) }),
|
|
301
|
+
replace: (site, edited) => save({ sites: withSiteReplaced(settings.sites, site, edited) }),
|
|
302
|
+
t, // the page's messages, under the standard keys below
|
|
259
303
|
});
|
|
260
304
|
sites.render(settings.sites);
|
|
261
305
|
|
|
262
|
-
//
|
|
263
|
-
|
|
306
|
+
// A short list with an add row (chrome://settings): `form` instead of `addButton`.
|
|
307
|
+
// <form class="row add" id="add-site"><input type="text" …><button type="submit">Add</button></form>
|
|
264
308
|
```
|
|
265
309
|
|
|
266
|
-
`
|
|
267
|
-
|
|
268
|
-
|
|
310
|
+
`withSite` adds a site at the end, once (added again, it moves there); `withoutSite` takes it out;
|
|
311
|
+
`withSiteReplaced` edits it in place, merging it with a duplicate. A list that needs site access
|
|
312
|
+
asks for it in `add`/`replace` (the dialog's click still counts as the person's) and gives it back
|
|
313
|
+
in `remove`. `add` resolving to false keeps the add row's text (access refused); `invalid(text)`
|
|
314
|
+
hears what isn't a site in the add row; the dialog keeps its Add off with “Enter a site, like
|
|
315
|
+
example.com” under the field.
|
|
316
|
+
|
|
317
|
+
The words: Chrome's English (`SITE_LIST_WORDS`) without `t`; with it, the page's messages under
|
|
318
|
+
these keys (`SITE_LIST_MESSAGES`), so every extension's `_locales` names them alike. `words`
|
|
319
|
+
overrides only those that differ.
|
|
320
|
+
|
|
321
|
+
| Word | Key | English |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `add` | `optionsAdd` | Add |
|
|
324
|
+
| `addSite` | `optionsAddSite` | Add site |
|
|
325
|
+
| `editSite` | `optionsEditSite` | Edit site |
|
|
326
|
+
| `edit` | `optionsEdit` | Edit |
|
|
327
|
+
| `remove` | `optionsRemove` | Remove |
|
|
328
|
+
| `save` | `optionsSave` | Save |
|
|
329
|
+
| `cancel` | `optionsCancel` | Cancel |
|
|
330
|
+
| `site` | `optionsSiteLabel` | Site |
|
|
331
|
+
| `invalid` | `optionsSiteInvalid` | Enter a site, like example.com |
|
|
332
|
+
| `moreActions` | `optionsMoreActions` | More actions for `$SITE$` (content `$1`; the ⋮ button) |
|
|
333
|
+
| `removeLabel` | `optionsRemoveLabel` | Remove `$SITE$` (content `$1`; the add row's Remove button) |
|
|
334
|
+
|
|
335
|
+
A list with an Add button needs every key but `optionsRemoveLabel`; one with an add row needs
|
|
336
|
+
`optionsRemove` and `optionsRemoveLabel`. A locale test can check them against `SITE_LIST_MESSAGES`.
|
|
269
337
|
|
|
270
338
|
## Build helpers (`@arraypress/extension-kit/build`)
|
|
271
339
|
|
|
@@ -300,7 +368,6 @@ except those listed, each with a reason:
|
|
|
300
368
|
| `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
|
|
301
369
|
| `extensionProfile(chromium, dir, options)` | A profile to start the browser on more than once (a restart, a first start with a changed manifest): `{dir, launch(), remove()}`. |
|
|
302
370
|
| `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()`. A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
|
|
303
|
-
| `within(promise, ms, fallback)` | The value in time, or the fallback (late or failed); never rejects; its timer cleared. |
|
|
304
371
|
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)`. |
|
|
305
372
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
306
373
|
| `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
|
|
@@ -340,7 +407,7 @@ except those listed, each with a reason:
|
|
|
340
407
|
| Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
|
|
341
408
|
| Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
|
|
342
409
|
| ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
|
|
343
|
-
| Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (
|
|
410
|
+
| Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (`askText`, `askFields`) |
|
|
344
411
|
| Toast (cr-toast) | `<div class="toast" popover="manual">text <button>Undo</button></div>` (or `createToast`); with `hidden` instead of `popover`, it shows over the page but not the top layer |
|
|
345
412
|
| Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
|
|
346
413
|
| Side panel page | `<body class="side-panel">`: the panel's colours, a column 100vh high |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/extension-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Shared pieces for Chrome extensions: Chrome's own page styles, a Chrome Sync settings store, the light/dark toolbar glyph, About links, and build and test helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/build/vendor.js
CHANGED
|
@@ -28,7 +28,7 @@ const SOURCE = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
|
28
28
|
const KIT = '@arraypress/extension-kit';
|
|
29
29
|
|
|
30
30
|
/** What a vendored copy of the kit holds, relative to `src/`: everything an extension page or worker may load. */
|
|
31
|
-
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'commands', 'dom', 'settings', 'sites', 'styles', 'toolbar', 'ui']);
|
|
31
|
+
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'commands', 'dom', 'settings', 'sites', 'styles', 'time', 'toolbar', 'ui']);
|
|
32
32
|
|
|
33
33
|
/** The kit's stylesheet, an absolute path in the installed package. */
|
|
34
34
|
export const STYLESHEET = path.join(SOURCE, 'styles', 'chrome-ui.css');
|
package/src/dom/i18n.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* `data-i18n-<attribute>="key"` (sets that attribute, e.g.
|
|
5
5
|
* `data-i18n-placeholder`, `data-i18n-aria-label`).
|
|
6
6
|
*
|
|
7
|
+
* The extension's name lives in one message, `extShortName`; a message that
|
|
8
|
+
* names the extension takes it as a placeholder whose content is `$1`
|
|
9
|
+
* (`"About $NAME$"`), and every lookup made without substitutions fills it
|
|
10
|
+
* in, in markup and script alike.
|
|
11
|
+
*
|
|
7
12
|
* @module dom/i18n
|
|
8
13
|
*/
|
|
9
14
|
export type I18nApi = {
|
|
@@ -19,18 +24,21 @@ export type I18nApi = {
|
|
|
19
24
|
*/
|
|
20
25
|
/**
|
|
21
26
|
* A message lookup that never comes back blank: a missing key shows as
|
|
22
|
-
* itself, so a typo is visible on screen instead of an empty label.
|
|
27
|
+
* itself, so a typo is visible on screen instead of an empty label. Without
|
|
28
|
+
* substitutions, `$1` is the extension's name (`extShortName`).
|
|
23
29
|
*
|
|
24
30
|
* @param {Pick<I18nApi, 'getMessage'>} i18n - `chrome.i18n`.
|
|
25
31
|
* @returns {(key: string, substitutions?: string | string[]) => string} Looks a key up.
|
|
26
32
|
* @example
|
|
27
33
|
* const t = translator(chrome.i18n);
|
|
28
34
|
* t('optionsVersion', ['1.0.0']); // 'Version 1.0.0'
|
|
35
|
+
* t('optionsAbout'); // 'About Upright', from "About $NAME$"
|
|
29
36
|
*/
|
|
30
37
|
export declare function translator(i18n: Pick<I18nApi, 'getMessage'>): (key: string, substitutions?: string | string[]) => string;
|
|
31
38
|
/**
|
|
32
|
-
* Fill every `data-i18n*` attribute under `root
|
|
33
|
-
* set its `lang` to
|
|
39
|
+
* Fill every `data-i18n*` attribute under `root` (a message that names the
|
|
40
|
+
* extension gets its name), and, for a whole document, set its `lang` to
|
|
41
|
+
* Chrome's UI language.
|
|
34
42
|
*
|
|
35
43
|
* @param {ParentNode} root - A document or an element.
|
|
36
44
|
* @param {I18nApi} i18n - `chrome.i18n`.
|
package/src/dom/i18n.js
CHANGED
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
* `data-i18n-<attribute>="key"` (sets that attribute, e.g.
|
|
5
5
|
* `data-i18n-placeholder`, `data-i18n-aria-label`).
|
|
6
6
|
*
|
|
7
|
+
* The extension's name lives in one message, `extShortName`; a message that
|
|
8
|
+
* names the extension takes it as a placeholder whose content is `$1`
|
|
9
|
+
* (`"About $NAME$"`), and every lookup made without substitutions fills it
|
|
10
|
+
* in, in markup and script alike.
|
|
11
|
+
*
|
|
7
12
|
* @module dom/i18n
|
|
8
13
|
*/
|
|
9
14
|
|
|
15
|
+
/** The message that holds the extension's name. */
|
|
16
|
+
const NAME_MESSAGE = 'extShortName';
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* `chrome.i18n`, as far as pages need it.
|
|
12
20
|
*
|
|
@@ -17,21 +25,26 @@
|
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* A message lookup that never comes back blank: a missing key shows as
|
|
20
|
-
* itself, so a typo is visible on screen instead of an empty label.
|
|
28
|
+
* itself, so a typo is visible on screen instead of an empty label. Without
|
|
29
|
+
* substitutions, `$1` is the extension's name (`extShortName`).
|
|
21
30
|
*
|
|
22
31
|
* @param {Pick<I18nApi, 'getMessage'>} i18n - `chrome.i18n`.
|
|
23
32
|
* @returns {(key: string, substitutions?: string | string[]) => string} Looks a key up.
|
|
24
33
|
* @example
|
|
25
34
|
* const t = translator(chrome.i18n);
|
|
26
35
|
* t('optionsVersion', ['1.0.0']); // 'Version 1.0.0'
|
|
36
|
+
* t('optionsAbout'); // 'About Upright', from "About $NAME$"
|
|
27
37
|
*/
|
|
28
38
|
export function translator(i18n) {
|
|
29
|
-
|
|
39
|
+
const name = i18n.getMessage(NAME_MESSAGE);
|
|
40
|
+
const named = name ? [name] : undefined;
|
|
41
|
+
return (key, substitutions = named) => i18n.getMessage(key, substitutions) || key;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
|
-
* Fill every `data-i18n*` attribute under `root
|
|
34
|
-
* set its `lang` to
|
|
45
|
+
* Fill every `data-i18n*` attribute under `root` (a message that names the
|
|
46
|
+
* extension gets its name), and, for a whole document, set its `lang` to
|
|
47
|
+
* Chrome's UI language.
|
|
35
48
|
*
|
|
36
49
|
* @param {ParentNode} root - A document or an element.
|
|
37
50
|
* @param {I18nApi} i18n - `chrome.i18n`.
|
package/src/dom/setup.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type PageSetup = {
|
|
|
13
13
|
*/
|
|
14
14
|
title?: string;
|
|
15
15
|
/**
|
|
16
|
-
* The title's placeholders
|
|
16
|
+
* The title's placeholders. Default: the extension's name (`translator`).
|
|
17
17
|
*/
|
|
18
18
|
substitutions?: string | string[];
|
|
19
19
|
/**
|
|
@@ -32,7 +32,7 @@ export type PageSetup = {
|
|
|
32
32
|
*
|
|
33
33
|
* @typedef {object} PageSetup
|
|
34
34
|
* @property {string} [title] The title's message (“optionsTitle”). Default: the title is left as it is.
|
|
35
|
-
* @property {string | string[]} [substitutions] The title's placeholders
|
|
35
|
+
* @property {string | string[]} [substitutions] The title's placeholders. Default: the extension's name (`translator`).
|
|
36
36
|
* @property {Document} [doc] The page. Default `document`.
|
|
37
37
|
* @property {SchemeMedia} [media] The scheme to report. Default the page's `matchMedia(DARK_QUERY)`.
|
|
38
38
|
*/
|
|
@@ -45,7 +45,8 @@ export type PageSetup = {
|
|
|
45
45
|
* @returns {(key: string, substitutions?: string | string[]) => string} The page's message lookup
|
|
46
46
|
* (`translator(chrome.i18n)`).
|
|
47
47
|
* @example
|
|
48
|
-
* const t = setUpPage(chrome, { title: 'optionsTitle'
|
|
48
|
+
* const t = setUpPage(chrome, { title: 'optionsTitle' }); // "$NAME$ settings": 'Upright settings'
|
|
49
|
+
* t('optionsGetHelp'); // "Get help with $NAME$": 'Get help with Upright'
|
|
49
50
|
* // The popup, whose title Chrome doesn't show:
|
|
50
51
|
* const t = setUpPage(chrome);
|
|
51
52
|
*/
|
package/src/dom/setup.js
CHANGED
|
@@ -17,7 +17,7 @@ import { DARK_QUERY, watchColorScheme } from '../toolbar/scheme.js';
|
|
|
17
17
|
*
|
|
18
18
|
* @typedef {object} PageSetup
|
|
19
19
|
* @property {string} [title] The title's message (“optionsTitle”). Default: the title is left as it is.
|
|
20
|
-
* @property {string | string[]} [substitutions] The title's placeholders
|
|
20
|
+
* @property {string | string[]} [substitutions] The title's placeholders. Default: the extension's name (`translator`).
|
|
21
21
|
* @property {Document} [doc] The page. Default `document`.
|
|
22
22
|
* @property {SchemeMedia} [media] The scheme to report. Default the page's `matchMedia(DARK_QUERY)`.
|
|
23
23
|
*/
|
|
@@ -31,7 +31,8 @@ import { DARK_QUERY, watchColorScheme } from '../toolbar/scheme.js';
|
|
|
31
31
|
* @returns {(key: string, substitutions?: string | string[]) => string} The page's message lookup
|
|
32
32
|
* (`translator(chrome.i18n)`).
|
|
33
33
|
* @example
|
|
34
|
-
* const t = setUpPage(chrome, { title: 'optionsTitle'
|
|
34
|
+
* const t = setUpPage(chrome, { title: 'optionsTitle' }); // "$NAME$ settings": 'Upright settings'
|
|
35
|
+
* t('optionsGetHelp'); // "Get help with $NAME$": 'Get help with Upright'
|
|
35
36
|
* // The popup, whose title Chrome doesn't show:
|
|
36
37
|
* const t = setUpPage(chrome);
|
|
37
38
|
*/
|
package/src/index.d.ts
CHANGED
|
@@ -20,19 +20,22 @@ export { setUpPage } from './dom/setup.js';
|
|
|
20
20
|
export { createSettingsSaver } from './settings/saver.js';
|
|
21
21
|
export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
|
|
22
22
|
export { createSettingsStore } from './settings/store.js';
|
|
23
|
-
export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin } from './sites/sites.js';
|
|
23
|
+
export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite, } from './sites/sites.js';
|
|
24
|
+
export { within } from './time/time.js';
|
|
24
25
|
export { POLL_MS, watchSchemeOffscreen } from './toolbar/offscreen.js';
|
|
25
26
|
export { DARK_QUERY, TOOLBAR_DARK_KEY, TOOLBAR_SCHEME, reportColorScheme, schemeSender, watchColorScheme } from './toolbar/scheme.js';
|
|
26
27
|
export { OFFSCREEN_PATH, TOOLBAR_ICON_DIR, createToolbarTheme, startToolbarTheme, toolbarIcon, watchFromOffscreen, } from './toolbar/theme.js';
|
|
27
28
|
export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from './ui/menu.js';
|
|
28
|
-
export { createSiteList } from './ui/site-list.js';
|
|
29
|
-
export { askText, checkText } from './ui/text-dialog.js';
|
|
29
|
+
export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
|
|
30
|
+
export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
|
|
30
31
|
export { TOAST_MS, createToast } from './ui/toast.js';
|
|
31
32
|
export type ActionMenu = import('./ui/menu.js').ActionMenu;
|
|
32
33
|
export type MenuEntry = import('./ui/menu.js').MenuEntry;
|
|
33
34
|
export type MenuItem = import('./ui/menu.js').MenuItem;
|
|
34
35
|
export type MenuPlace = import('./ui/menu.js').MenuPlace;
|
|
35
36
|
export type SiteList = import('./ui/site-list.js').SiteList;
|
|
37
|
+
export type FieldRequest = import('./ui/text-dialog.js').FieldRequest;
|
|
38
|
+
export type FieldsRequest = import('./ui/text-dialog.js').FieldsRequest;
|
|
36
39
|
export type TextRequest = import('./ui/text-dialog.js').TextRequest;
|
|
37
40
|
export type Toast = import('./ui/toast.js').Toast;
|
|
38
41
|
/**
|
|
@@ -43,6 +46,8 @@ export type Toast = import('./ui/toast.js').Toast;
|
|
|
43
46
|
* @typedef {import('./ui/menu.js').MenuItem} MenuItem
|
|
44
47
|
* @typedef {import('./ui/menu.js').MenuPlace} MenuPlace
|
|
45
48
|
* @typedef {import('./ui/site-list.js').SiteList} SiteList
|
|
49
|
+
* @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
|
|
50
|
+
* @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
|
|
46
51
|
* @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
|
|
47
52
|
* @typedef {import('./ui/toast.js').Toast} Toast
|
|
48
53
|
*/
|
package/src/index.js
CHANGED
|
@@ -21,15 +21,18 @@ export { setUpPage } from './dom/setup.js';
|
|
|
21
21
|
export { createSettingsSaver } from './settings/saver.js';
|
|
22
22
|
export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
|
|
23
23
|
export { createSettingsStore } from './settings/store.js';
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite,
|
|
26
|
+
} from './sites/sites.js';
|
|
27
|
+
export { within } from './time/time.js';
|
|
25
28
|
export { POLL_MS, watchSchemeOffscreen } from './toolbar/offscreen.js';
|
|
26
29
|
export { DARK_QUERY, TOOLBAR_DARK_KEY, TOOLBAR_SCHEME, reportColorScheme, schemeSender, watchColorScheme } from './toolbar/scheme.js';
|
|
27
30
|
export {
|
|
28
31
|
OFFSCREEN_PATH, TOOLBAR_ICON_DIR, createToolbarTheme, startToolbarTheme, toolbarIcon, watchFromOffscreen,
|
|
29
32
|
} from './toolbar/theme.js';
|
|
30
33
|
export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from './ui/menu.js';
|
|
31
|
-
export { createSiteList } from './ui/site-list.js';
|
|
32
|
-
export { askText, checkText } from './ui/text-dialog.js';
|
|
34
|
+
export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
|
|
35
|
+
export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
|
|
33
36
|
export { TOAST_MS, createToast } from './ui/toast.js';
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -40,6 +43,8 @@ export { TOAST_MS, createToast } from './ui/toast.js';
|
|
|
40
43
|
* @typedef {import('./ui/menu.js').MenuItem} MenuItem
|
|
41
44
|
* @typedef {import('./ui/menu.js').MenuPlace} MenuPlace
|
|
42
45
|
* @typedef {import('./ui/site-list.js').SiteList} SiteList
|
|
46
|
+
* @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
|
|
47
|
+
* @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
|
|
43
48
|
* @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
|
|
44
49
|
* @typedef {import('./ui/toast.js').Toast} Toast
|
|
45
50
|
*/
|
package/src/sites/sites.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* "https://www.Example.com/shop") and as pages report them (`location.href`):
|
|
4
4
|
* the origin an extension asks Chrome for access to, the host a per-site
|
|
5
5
|
* on/off list keeps, whether a page is on a listed site (subdomains
|
|
6
|
-
* included),
|
|
6
|
+
* included), the match patterns for both, and a list with a site added,
|
|
7
|
+
* taken out or edited. Pure.
|
|
7
8
|
*
|
|
8
9
|
* A site is a web host: http or https, with a dot in its name (or
|
|
9
10
|
* `localhost`). Text without a scheme is read as https.
|
|
@@ -80,3 +81,34 @@ export declare function originPattern(url: string): string;
|
|
|
80
81
|
* siteLabel('http://example.com'); // 'http://example.com'
|
|
81
82
|
*/
|
|
82
83
|
export declare function siteLabel(site: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* A site list with `site` added: at the end, once (a site added again moves
|
|
86
|
+
* there).
|
|
87
|
+
*
|
|
88
|
+
* @param {readonly string[]} sites - The list.
|
|
89
|
+
* @param {string} site - The site to add.
|
|
90
|
+
* @returns {string[]} A new list.
|
|
91
|
+
* @example
|
|
92
|
+
* withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com']
|
|
93
|
+
*/
|
|
94
|
+
export declare function withSite(sites: readonly string[], site: string): string[];
|
|
95
|
+
/**
|
|
96
|
+
* A site list without `site`.
|
|
97
|
+
*
|
|
98
|
+
* @param {readonly string[]} sites - The list.
|
|
99
|
+
* @param {string} site - The site to take out.
|
|
100
|
+
* @returns {string[]} A new list.
|
|
101
|
+
*/
|
|
102
|
+
export declare function withoutSite(sites: readonly string[], site: string): string[];
|
|
103
|
+
/**
|
|
104
|
+
* A site list with `site` edited to `edited`, in its place, once (an edit
|
|
105
|
+
* to a site already listed merges the two).
|
|
106
|
+
*
|
|
107
|
+
* @param {readonly string[]} sites - The list.
|
|
108
|
+
* @param {string} site - The site as it was.
|
|
109
|
+
* @param {string} edited - What it was edited to.
|
|
110
|
+
* @returns {string[]} A new list.
|
|
111
|
+
* @example
|
|
112
|
+
* withSiteReplaced(['a.com', 'b.com'], 'a.com', 'b.com'); // ['b.com']
|
|
113
|
+
*/
|
|
114
|
+
export declare function withSiteReplaced(sites: readonly string[], site: string, edited: string): string[];
|
package/src/sites/sites.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* "https://www.Example.com/shop") and as pages report them (`location.href`):
|
|
4
4
|
* the origin an extension asks Chrome for access to, the host a per-site
|
|
5
5
|
* on/off list keeps, whether a page is on a listed site (subdomains
|
|
6
|
-
* included),
|
|
6
|
+
* included), the match patterns for both, and a list with a site added,
|
|
7
|
+
* taken out or edited. Pure.
|
|
7
8
|
*
|
|
8
9
|
* A site is a web host: http or https, with a dot in its name (or
|
|
9
10
|
* `localhost`). Text without a scheme is read as https.
|
|
@@ -126,3 +127,43 @@ export function originPattern(url) {
|
|
|
126
127
|
export function siteLabel(site) {
|
|
127
128
|
return site.replace(/^https:\/\//, '');
|
|
128
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A site list with `site` added: at the end, once (a site added again moves
|
|
133
|
+
* there).
|
|
134
|
+
*
|
|
135
|
+
* @param {readonly string[]} sites - The list.
|
|
136
|
+
* @param {string} site - The site to add.
|
|
137
|
+
* @returns {string[]} A new list.
|
|
138
|
+
* @example
|
|
139
|
+
* withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com']
|
|
140
|
+
*/
|
|
141
|
+
export function withSite(sites, site) {
|
|
142
|
+
return [...withoutSite(sites, site), site];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A site list without `site`.
|
|
147
|
+
*
|
|
148
|
+
* @param {readonly string[]} sites - The list.
|
|
149
|
+
* @param {string} site - The site to take out.
|
|
150
|
+
* @returns {string[]} A new list.
|
|
151
|
+
*/
|
|
152
|
+
export function withoutSite(sites, site) {
|
|
153
|
+
return sites.filter((s) => s !== site);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A site list with `site` edited to `edited`, in its place, once (an edit
|
|
158
|
+
* to a site already listed merges the two).
|
|
159
|
+
*
|
|
160
|
+
* @param {readonly string[]} sites - The list.
|
|
161
|
+
* @param {string} site - The site as it was.
|
|
162
|
+
* @param {string} edited - What it was edited to.
|
|
163
|
+
* @returns {string[]} A new list.
|
|
164
|
+
* @example
|
|
165
|
+
* withSiteReplaced(['a.com', 'b.com'], 'a.com', 'b.com'); // ['b.com']
|
|
166
|
+
*/
|
|
167
|
+
export function withSiteReplaced(sites, site, edited) {
|
|
168
|
+
return [...new Set(sites.map((s) => (s === site ? edited : s)))];
|
|
169
|
+
}
|
package/src/testing/browser.js
CHANGED
package/src/testing/e2e.js
CHANGED
|
@@ -12,7 +12,7 @@ import assert from 'node:assert/strict';
|
|
|
12
12
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { deadline } from '
|
|
15
|
+
import { deadline } from '../time/time.js';
|
|
16
16
|
|
|
17
17
|
/** @import { BrowserContext, BrowserType, CDPSession, Page, Worker } from 'playwright' */
|
|
18
18
|
|
package/src/testing/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
|
|
|
10
10
|
export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
|
|
11
11
|
export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until, } from './e2e.js';
|
|
12
12
|
export { eachLimited } from './pool.js';
|
|
13
|
-
export { within } from './time.js';
|
|
14
13
|
export { serve } from './server.js';
|
|
15
14
|
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
16
15
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
package/src/testing/index.js
CHANGED
|
@@ -13,7 +13,6 @@ export {
|
|
|
13
13
|
CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
|
|
14
14
|
} from './e2e.js';
|
|
15
15
|
export { eachLimited } from './pool.js';
|
|
16
|
-
export { within } from './time.js';
|
|
17
16
|
export { serve } from './server.js';
|
|
18
17
|
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
19
18
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Time limits for
|
|
3
|
-
*
|
|
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).
|
|
4
5
|
*
|
|
5
|
-
* @module
|
|
6
|
+
* @module time/time
|
|
6
7
|
*/
|
|
7
8
|
/**
|
|
8
9
|
* Settle `promise` within `ms`, or settle the way `late` says. The timer is
|
|
@@ -19,9 +20,9 @@
|
|
|
19
20
|
export declare function deadline<T>(promise: Promise<T>, ms: number, late: (resolve: (value: T) => void, reject: (error: Error) => void) => void): Promise<T>;
|
|
20
21
|
/**
|
|
21
22
|
* A promise's value within a time limit, or `fallback` when it's late or
|
|
22
|
-
* fails: for a step
|
|
23
|
-
* that stopped responding, a close that never returns).
|
|
24
|
-
* isn't stopped; the timer is cleared once it settles.
|
|
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.
|
|
25
26
|
*
|
|
26
27
|
* @template T
|
|
27
28
|
* @param {Promise<T>} promise - The work.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Time limits for
|
|
3
|
-
*
|
|
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).
|
|
4
5
|
*
|
|
5
|
-
* @module
|
|
6
|
+
* @module time/time
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -28,9 +29,9 @@ export function deadline(promise, ms, late) {
|
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* A promise's value within a time limit, or `fallback` when it's late or
|
|
31
|
-
* fails: for a step
|
|
32
|
-
* that stopped responding, a close that never returns).
|
|
33
|
-
* isn't stopped; the timer is cleared once it settles.
|
|
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.
|
|
34
35
|
*
|
|
35
36
|
* @template T
|
|
36
37
|
* @param {Promise<T>} promise - The work.
|