@arraypress/extension-kit 1.7.0 → 1.9.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 +64 -3
- package/package.json +1 -1
- package/src/build/store-frame.d.ts +10 -91
- package/src/build/store-frame.js +11 -159
- package/src/build/vendor.js +1 -1
- package/src/content/overlay.d.ts +47 -0
- package/src/content/overlay.js +42 -0
- package/src/content/slices.d.ts +16 -1
- package/src/content/slices.js +29 -10
- package/src/frame/window.d.ts +182 -0
- package/src/frame/window.js +299 -0
- package/src/index.d.ts +18 -3
- package/src/index.js +13 -3
- package/src/panel/side-panel.d.ts +35 -0
- package/src/panel/side-panel.js +41 -0
- package/src/scripting/call.d.ts +63 -0
- package/src/scripting/call.js +57 -0
- package/src/settings/saver.d.ts +13 -0
- package/src/settings/saver.js +11 -0
- package/src/testing/heartbeat.d.ts +23 -0
- package/src/testing/heartbeat.js +43 -0
- package/src/testing/index.d.ts +2 -0
- package/src/testing/index.js +2 -0
- package/src/testing/prompt.d.ts +20 -0
- package/src/testing/prompt.js +29 -0
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
|
|
|
13
13
|
- **Chrome's page components**: the ⋮ action menu and its row button, the text dialog
|
|
14
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
|
+
- **Chrome's window around a capture**: the tab strip and toolbar, light or dark, as HTML for store
|
|
17
|
+
screenshots and as an SVG image an extension page draws on a canvas.
|
|
16
18
|
- **Sites**: what people type into a per-site list, and whether a page is on one.
|
|
17
19
|
- **Chrome's on-device model** (Gemini Nano, the Prompt API): availability, a download from the
|
|
18
20
|
person's click with progress, one base session with a `clone()` per question, a time limit on
|
|
@@ -108,6 +110,8 @@ chrome.storage.onChanged.addListener((changes, area) => {
|
|
|
108
110
|
const saver = createSettingsSaver(store, chrome.storage, { onSaved: showSaved });
|
|
109
111
|
saver.save({ menuStyle: 'full' });
|
|
110
112
|
addEventListener('pagehide', () => saver.flush());
|
|
113
|
+
// Changed in another tab or on another computer: shown, with this page's unwritten changes over it.
|
|
114
|
+
saver.follow((settings) => show(settings), chrome.storage.onChanged);
|
|
111
115
|
```
|
|
112
116
|
|
|
113
117
|
| Store member | What it does |
|
|
@@ -183,6 +187,38 @@ takes it as a placeholder whose content is `$1`, and every lookup without substi
|
|
|
183
187
|
<h2 class="section-title" data-i18n="optionsAbout"></h2> <!-- About Upright -->
|
|
184
188
|
```
|
|
185
189
|
|
|
190
|
+
## Chrome's window around a capture
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { WINDOW_BARS, letterIcon, windowChromeSvg } from '../vendor/extension-kit/index.js';
|
|
194
|
+
|
|
195
|
+
const svg = windowChromeSvg({ tabs: [{ title: 'Example', icon: letterIcon('E', '#1a73e8'), active: true }], url: 'example.com/a', theme: 'dark', w: 1280 });
|
|
196
|
+
const bars = new Image();
|
|
197
|
+
bars.src = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
198
|
+
await bars.decode();
|
|
199
|
+
context.drawImage(bars, 0, 0, 1280 * scale, (WINDOW_BARS.strip + WINDOW_BARS.toolbar) * scale); // the capture goes below
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The same macOS window the store screenshots use (`storeFrame.windowChrome`, now `frame/window.js`
|
|
203
|
+
in the runtime half): `windowChrome(options)` is its HTML, `windowChromeSvg(options)` that HTML in
|
|
204
|
+
an SVG image `w` wide and `WINDOW_BARS.strip + WINDOW_BARS.toolbar` (88px) high, drawn sharp at any
|
|
205
|
+
scale. Chrome draws an SVG image with HTML inside (`foreignObject`) without tainting the canvas
|
|
206
|
+
(checked in Chromium, `tests/frame-chromium.test.js`). Images in it must be `data:` URLs
|
|
207
|
+
(`letterIcon` makes one). `THEMES` holds the light and dark colours.
|
|
208
|
+
|
|
209
|
+
A plain macOS window (1.9.0) draws the same way: `macWindowSvg({ title, theme, w })` is its title
|
|
210
|
+
bar as macOS 26 and later draw it (measured from AppKit on macOS 27.2), `w` wide and
|
|
211
|
+
`MAC_WINDOW.titleBar` (32px) high: 14px traffic lights (the same markup as Chrome's strip), the
|
|
212
|
+
title after them, a hairline below. The canvas draws the rest: corners of `MAC_WINDOW.radius`
|
|
213
|
+
(16px, `NSThemeFrame`'s) and the theme's shadow, `THEMES[theme].windowShadow`
|
|
214
|
+
(`{ y, blur, color }` for a canvas's `shadowOffsetY`, `shadowBlur`, `shadowColor`; `THEMES[theme].shadow`
|
|
215
|
+
is the same shadow as CSS, the store frame's).
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
context.shadowColor = THEMES.light.windowShadow.color; // and its blur and y, at the scale
|
|
219
|
+
context.roundRect(x, y, w * scale, (MAC_WINDOW.titleBar + contentHeight) * scale, MAC_WINDOW.radius * scale);
|
|
220
|
+
```
|
|
221
|
+
|
|
186
222
|
## Sites
|
|
187
223
|
|
|
188
224
|
```js
|
|
@@ -198,6 +234,28 @@ withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list
|
|
|
198
234
|
A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
|
|
199
235
|
`localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
|
|
200
236
|
|
|
237
|
+
## A page's side
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
// In the worker or a page: call content/main.js's `scan` in the tab's frames (main.js and what it
|
|
241
|
+
// imports listed in web_accessible_resources). The module stays loaded between calls.
|
|
242
|
+
const frames = await callInPage(chrome.scripting, { tabId, allFrames: true }, 'content/main.js', 'scan', [options]);
|
|
243
|
+
for (const { frameId, result } of frames) show(frameId, result);
|
|
244
|
+
|
|
245
|
+
// From the toolbar click: the tab's own side panel, panel/panel.html?tab=<id>, nothing awaited first.
|
|
246
|
+
chrome.action.onClicked.addListener((tab) => openSidePanel(chrome.sidePanel, tab.id, 'panel/panel.html'));
|
|
247
|
+
|
|
248
|
+
// In the content script: a layer the page's styles can't touch (a closed shadow root).
|
|
249
|
+
const { root, remove } = pageOverlay(document, { name: 'my-inspector', css: ':host { position: fixed; inset: 0; pointer-events: none; }' });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`callInPage(scripting, target, module, name, args, {immediately})` resolves to each frame's
|
|
253
|
+
`{frameId, result}` (frames Chrome couldn't run in left out) and rejects when it can run in none;
|
|
254
|
+
`immediately` (default true) doesn't wait for the page to be idle. `openSidePanel` sets the tab's
|
|
255
|
+
panel to `path?tab=<id>` (`sidePanelPath`) and opens it in the same moment: Chrome opens a panel
|
|
256
|
+
only while the click counts. `pageOverlay` puts a host element of its own name (`all: initial`)
|
|
257
|
+
on the root element, replacing one a run before left; `root` is its closed shadow root, `css` first.
|
|
258
|
+
|
|
201
259
|
## A content script on every site
|
|
202
260
|
|
|
203
261
|
```js
|
|
@@ -286,7 +344,8 @@ const stop = watchChanges(document.body, {
|
|
|
286
344
|
|
|
287
345
|
`inSlices(root, work, {blockChars, sliceMs, pause})` cuts a page into blocks (`blocksOf`) and
|
|
288
346
|
hands the main thread back whenever a slice has run its time, so a 2,000-item listing never
|
|
289
|
-
freezes. `
|
|
347
|
+
freezes. `eachInSlices(items, work, {sliceMs, pause})` does the same for any list (each element's
|
|
348
|
+
style read, each text measured): `work(item, index)`. `watchChanges` hands over what was added or changed a little after the page stops
|
|
290
349
|
changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
|
|
291
350
|
default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
|
|
292
351
|
imports them directly (list them in `web_accessible_resources`).
|
|
@@ -456,7 +515,7 @@ A list with an Add button needs every key but `optionsRemoveLabel`; one with an
|
|
|
456
515
|
| `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
|
|
457
516
|
| `buildTestCopy({from, to, hook, manifest})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed. |
|
|
458
517
|
| `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
|
|
459
|
-
| `storeFrame` | Store screenshots: `canvas`, `windowChrome`, `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
|
|
518
|
+
| `storeFrame` | Store screenshots: `canvas`, `windowChrome` (and `windowChromeSvg`, from `frame/window.js`), `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
|
|
460
519
|
| `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
|
|
461
520
|
| `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
|
|
462
521
|
|
|
@@ -480,6 +539,8 @@ except those listed, each with a reason:
|
|
|
480
539
|
| `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()}`. |
|
|
481
540
|
| `runChecks(runner, body, cleanUps)` | A script's checks: an error outside any check fails, the clean-ups always run (each within 30 s), then the tally. |
|
|
482
541
|
| `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales` (a locale over en, named placeholders filled), for unit tests. |
|
|
542
|
+
| `heartbeat(page)` | A timer beating in a page: `stop()` gives the longest the main thread was held (ms), for performance checks. |
|
|
543
|
+
| `playPermissionPrompt(answers)` | Chrome's permission prompt played in a page (`page.evaluate` or `addInitScript`): each request recorded on `window.asked` with whether a click was behind it, answered from `answers`. |
|
|
483
544
|
| `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. |
|
|
484
545
|
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)` (also in the main entry). |
|
|
485
546
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
@@ -543,7 +604,7 @@ chrome://bookmarks in Chromium 156 / Chrome 157; the sources are listed at the t
|
|
|
543
604
|
## Development
|
|
544
605
|
|
|
545
606
|
`npm test` runs ESLint, a strict TypeScript check of the JSDoc (`checkJs`), the tests (the menu,
|
|
546
|
-
the dialog, the toast, the dialog site list, icon rendering and the Chrome readers in Playwright's
|
|
607
|
+
the dialog, the toast, the dialog site list, the window frame, icon rendering and the Chrome readers in Playwright's
|
|
547
608
|
Chromium, the rest in Node and jsdom), and the
|
|
548
609
|
copied-code guard. The `.d.ts` files are generated from the JSDoc: `npm run types` writes them, and
|
|
549
610
|
the type check fails while one is stale (it regenerates them into `.types/` and compares).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/extension-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Shared pieces for Chrome extensions: Chrome's own page styles and components, a Chrome Sync settings store, the light/dark toolbar glyph, Chrome's on-device model, content-script helpers, About links, and build and test helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -10,19 +10,27 @@
|
|
|
10
10
|
* Playwright at `geometry(zoom).content`, then screenshot `canvas({caption, chrome:
|
|
11
11
|
* windowChrome({tabs, url}), content})` at {@link W}×{@link H}.
|
|
12
12
|
*
|
|
13
|
+
* The window itself is `frame/window.js`, in the runtime half, so an extension page can draw it
|
|
14
|
+
* around a capture too; it's re-exported here.
|
|
15
|
+
*
|
|
13
16
|
* @module build/store-frame
|
|
14
17
|
*/
|
|
18
|
+
export { THEMES, letterIcon, windowChrome, windowChromeSvg } from '../frame/window.js';
|
|
19
|
+
export type Theme = import('../frame/window.js').Theme;
|
|
20
|
+
export type Tab = import('../frame/window.js').Tab;
|
|
21
|
+
/** @typedef {import('../frame/window.js').Theme} Theme */
|
|
22
|
+
/** @typedef {import('../frame/window.js').Tab} Tab */
|
|
15
23
|
/** The store screenshot's width. */
|
|
16
24
|
export declare const W = 1280;
|
|
17
25
|
/** The store screenshot's height. */
|
|
18
26
|
export declare const H = 800;
|
|
19
27
|
/** Where the window sits: its left edge, top edge, full width, and tab strip and toolbar heights. */
|
|
20
28
|
export declare const WIN: {
|
|
29
|
+
strip: 42;
|
|
30
|
+
toolbar: 46;
|
|
21
31
|
x: number;
|
|
22
32
|
y: number;
|
|
23
33
|
w: number;
|
|
24
|
-
strip: number;
|
|
25
|
-
toolbar: number;
|
|
26
34
|
};
|
|
27
35
|
export type Geometry = {
|
|
28
36
|
/**
|
|
@@ -57,95 +65,6 @@ export type Geometry = {
|
|
|
57
65
|
* @returns {Geometry} The window's unscaled width and the size to capture page content at.
|
|
58
66
|
*/
|
|
59
67
|
export declare function geometry(zoom?: number): Geometry;
|
|
60
|
-
/**
|
|
61
|
-
* A letter favicon for made-up sites (no real brands).
|
|
62
|
-
*
|
|
63
|
-
* @param {string} letter - The letter.
|
|
64
|
-
* @param {string} bg - Background colour.
|
|
65
|
-
* @param {string} [fg] - Letter colour. Default white.
|
|
66
|
-
* @returns {string} A data: URL of the SVG.
|
|
67
|
-
*/
|
|
68
|
-
export declare const letterIcon: (letter: string, bg: string, fg?: string) => string;
|
|
69
|
-
/** Chrome's light and dark window colours, and the canvas behind. */
|
|
70
|
-
export declare const THEMES: {
|
|
71
|
-
light: {
|
|
72
|
-
bg: string;
|
|
73
|
-
caption: string;
|
|
74
|
-
sub: string;
|
|
75
|
-
frame: string;
|
|
76
|
-
tabText: string;
|
|
77
|
-
tabTextInactive: string;
|
|
78
|
-
tabSep: string;
|
|
79
|
-
activeTab: string;
|
|
80
|
-
toolbar: string;
|
|
81
|
-
toolbarIcon: string;
|
|
82
|
-
omnibox: string;
|
|
83
|
-
url: string;
|
|
84
|
-
urlDim: string;
|
|
85
|
-
contentLine: string;
|
|
86
|
-
shadow: string;
|
|
87
|
-
};
|
|
88
|
-
dark: {
|
|
89
|
-
bg: string;
|
|
90
|
-
caption: string;
|
|
91
|
-
sub: string;
|
|
92
|
-
frame: string;
|
|
93
|
-
tabText: string;
|
|
94
|
-
tabTextInactive: string;
|
|
95
|
-
tabSep: string;
|
|
96
|
-
activeTab: string;
|
|
97
|
-
toolbar: string;
|
|
98
|
-
toolbarIcon: string;
|
|
99
|
-
omnibox: string;
|
|
100
|
-
url: string;
|
|
101
|
-
urlDim: string;
|
|
102
|
-
contentLine: string;
|
|
103
|
-
shadow: string;
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
export type Theme = keyof typeof THEMES;
|
|
107
|
-
export type Tab = {
|
|
108
|
-
/**
|
|
109
|
-
* Its title.
|
|
110
|
-
*/
|
|
111
|
-
title: string;
|
|
112
|
-
/**
|
|
113
|
-
* Its favicon's URL.
|
|
114
|
-
*/
|
|
115
|
-
icon: string;
|
|
116
|
-
/**
|
|
117
|
-
* The selected tab.
|
|
118
|
-
*/
|
|
119
|
-
active?: boolean;
|
|
120
|
-
/**
|
|
121
|
-
* Playing sound.
|
|
122
|
-
*/
|
|
123
|
-
audible?: boolean;
|
|
124
|
-
/**
|
|
125
|
-
* Muted.
|
|
126
|
-
*/
|
|
127
|
-
muted?: boolean;
|
|
128
|
-
};
|
|
129
|
-
/**
|
|
130
|
-
* The window's tab strip and toolbar, as HTML. Positions are window-relative.
|
|
131
|
-
*
|
|
132
|
-
* @param {object} options - What to draw.
|
|
133
|
-
* @param {readonly Tab[]} options.tabs - The tabs, left to right.
|
|
134
|
-
* @param {string} options.url - The address bar's text.
|
|
135
|
-
* @param {string} [options.urlHost] - The start of `url` to show in full; the rest is dimmed.
|
|
136
|
-
* @param {Theme} [options.theme] - Default light.
|
|
137
|
-
* @param {number} [options.w] - The window's width. Default the full window.
|
|
138
|
-
* @param {readonly string[]} [options.ext] - Toolbar extension icons' URLs.
|
|
139
|
-
* @returns {string} The HTML, with its own styles.
|
|
140
|
-
*/
|
|
141
|
-
export declare function windowChrome({ tabs, url, urlHost, theme, w, ext }: {
|
|
142
|
-
tabs: readonly Tab[];
|
|
143
|
-
url: string;
|
|
144
|
-
urlHost?: string;
|
|
145
|
-
theme?: Theme;
|
|
146
|
-
w?: number;
|
|
147
|
-
ext?: readonly string[];
|
|
148
|
-
}): string;
|
|
149
68
|
/**
|
|
150
69
|
* The macOS arrow pointer.
|
|
151
70
|
*
|
package/src/build/store-frame.js
CHANGED
|
@@ -10,16 +10,26 @@
|
|
|
10
10
|
* Playwright at `geometry(zoom).content`, then screenshot `canvas({caption, chrome:
|
|
11
11
|
* windowChrome({tabs, url}), content})` at {@link W}×{@link H}.
|
|
12
12
|
*
|
|
13
|
+
* The window itself is `frame/window.js`, in the runtime half, so an extension page can draw it
|
|
14
|
+
* around a capture too; it's re-exported here.
|
|
15
|
+
*
|
|
13
16
|
* @module build/store-frame
|
|
14
17
|
*/
|
|
15
18
|
|
|
19
|
+
import { THEMES, WINDOW_BARS, WINDOW_W } from '../frame/window.js';
|
|
20
|
+
|
|
21
|
+
export { THEMES, letterIcon, windowChrome, windowChromeSvg } from '../frame/window.js';
|
|
22
|
+
|
|
23
|
+
/** @typedef {import('../frame/window.js').Theme} Theme */
|
|
24
|
+
/** @typedef {import('../frame/window.js').Tab} Tab */
|
|
25
|
+
|
|
16
26
|
/** The store screenshot's width. */
|
|
17
27
|
export const W = 1280;
|
|
18
28
|
/** The store screenshot's height. */
|
|
19
29
|
export const H = 800;
|
|
20
30
|
const CAPTION_H = 112;
|
|
21
31
|
/** Where the window sits: its left edge, top edge, full width, and tab strip and toolbar heights. */
|
|
22
|
-
export const WIN = { x: 64, y: CAPTION_H, w:
|
|
32
|
+
export const WIN = { x: 64, y: CAPTION_H, w: WINDOW_W, ...WINDOW_BARS };
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* How a shot's window is laid out.
|
|
@@ -42,164 +52,6 @@ export function geometry(zoom = 1) {
|
|
|
42
52
|
return { zoom, w, content: { width: w, height: Math.round((H - WIN.y) / zoom) - WIN.strip - WIN.toolbar } };
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
/**
|
|
46
|
-
* @param {unknown} s - Text.
|
|
47
|
-
* @returns {string} It, escaped for HTML text and attributes.
|
|
48
|
-
*/
|
|
49
|
-
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
50
|
-
|
|
51
|
-
/** Material icon paths (24×24). */
|
|
52
|
-
const P = {
|
|
53
|
-
back: 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z',
|
|
54
|
-
forward: 'M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z',
|
|
55
|
-
reload: 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z',
|
|
56
|
-
close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
|
|
57
|
-
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
|
|
58
|
-
more: 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z',
|
|
59
|
-
puzzle: 'M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z',
|
|
60
|
-
star: 'M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z',
|
|
61
|
-
tune: 'M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z',
|
|
62
|
-
sound: 'M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z',
|
|
63
|
-
muted: 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z',
|
|
64
|
-
person: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z',
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A Material icon as inline SVG, in the text colour.
|
|
69
|
-
*
|
|
70
|
-
* @param {keyof typeof P} name - Which icon.
|
|
71
|
-
* @param {number} [size] - Width and height in px. Default 20.
|
|
72
|
-
* @returns {string} The SVG markup.
|
|
73
|
-
*/
|
|
74
|
-
const icon = (name, size = 20) =>
|
|
75
|
-
`<svg viewBox="0 0 24 24" width="${size}" height="${size}"><path fill="currentColor" d="${P[name]}"/></svg>`;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* A letter favicon for made-up sites (no real brands).
|
|
79
|
-
*
|
|
80
|
-
* @param {string} letter - The letter.
|
|
81
|
-
* @param {string} bg - Background colour.
|
|
82
|
-
* @param {string} [fg] - Letter colour. Default white.
|
|
83
|
-
* @returns {string} A data: URL of the SVG.
|
|
84
|
-
*/
|
|
85
|
-
export const letterIcon = (letter, bg, fg = '#fff') =>
|
|
86
|
-
`data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="4" fill="${bg}"/><text x="8" y="12" font-family="-apple-system,Helvetica,Arial" font-size="11" font-weight="700" text-anchor="middle" fill="${fg}">${letter}</text></svg>`)}`;
|
|
87
|
-
|
|
88
|
-
/** Chrome's light and dark window colours, and the canvas behind. */
|
|
89
|
-
export const THEMES = {
|
|
90
|
-
light: {
|
|
91
|
-
bg: '#eef2f8', caption: '#1f1f1f', sub: '#444746',
|
|
92
|
-
frame: '#dde3ea', tabText: '#1f1f1f', tabTextInactive: '#474747', tabSep: '#a8afb8', activeTab: '#ffffff',
|
|
93
|
-
toolbar: '#ffffff', toolbarIcon: '#474747', omnibox: '#e9eef6', url: '#1f1f1f', urlDim: '#5e5e5e',
|
|
94
|
-
contentLine: '#d3d7dc', shadow: '0 24px 60px rgba(31,45,70,.20), 0 0 0 .5px rgba(0,0,0,.18)',
|
|
95
|
-
},
|
|
96
|
-
dark: {
|
|
97
|
-
bg: '#1b1c1f', caption: '#e3e3e3', sub: '#c4c7c5',
|
|
98
|
-
frame: '#1f1f1f', tabText: '#e3e3e3', tabTextInactive: '#c7c7c7', tabSep: '#5f6368', activeTab: '#3c3c3c',
|
|
99
|
-
toolbar: '#3c3c3c', toolbarIcon: '#c7c7c7', omnibox: '#282828', url: '#e3e3e3', urlDim: '#a8a8a8',
|
|
100
|
-
contentLine: '#2a2a2a', shadow: '0 24px 60px rgba(0,0,0,.55), 0 0 0 .5px rgba(255,255,255,.14)',
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/** @typedef {keyof typeof THEMES} Theme */
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* One tab in the drawn window.
|
|
108
|
-
*
|
|
109
|
-
* @typedef {object} Tab
|
|
110
|
-
* @property {string} title Its title.
|
|
111
|
-
* @property {string} icon Its favicon's URL.
|
|
112
|
-
* @property {boolean} [active] The selected tab.
|
|
113
|
-
* @property {boolean} [audible] Playing sound.
|
|
114
|
-
* @property {boolean} [muted] Muted.
|
|
115
|
-
*/
|
|
116
|
-
|
|
117
|
-
const TAB_H = 34;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Lay out tabs like Chrome: equal widths up to 240px, shrinking to fit.
|
|
121
|
-
*
|
|
122
|
-
* @param {readonly Tab[]} tabs - The tabs.
|
|
123
|
-
* @param {number} w - The window's width.
|
|
124
|
-
* @returns {{ x: number, width: number }[]} Each tab's window-relative x and width.
|
|
125
|
-
*/
|
|
126
|
-
function tabLayout(tabs, w) {
|
|
127
|
-
const left = 84;
|
|
128
|
-
const right = 60;
|
|
129
|
-
const max = 240;
|
|
130
|
-
const avail = w - left - right - 36;
|
|
131
|
-
const width = Math.min(max, Math.floor(avail / tabs.length));
|
|
132
|
-
return tabs.map((_, i) => ({ x: left + i * width, width }));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* The window's tab strip and toolbar, as HTML. Positions are window-relative.
|
|
137
|
-
*
|
|
138
|
-
* @param {object} options - What to draw.
|
|
139
|
-
* @param {readonly Tab[]} options.tabs - The tabs, left to right.
|
|
140
|
-
* @param {string} options.url - The address bar's text.
|
|
141
|
-
* @param {string} [options.urlHost] - The start of `url` to show in full; the rest is dimmed.
|
|
142
|
-
* @param {Theme} [options.theme] - Default light.
|
|
143
|
-
* @param {number} [options.w] - The window's width. Default the full window.
|
|
144
|
-
* @param {readonly string[]} [options.ext] - Toolbar extension icons' URLs.
|
|
145
|
-
* @returns {string} The HTML, with its own styles.
|
|
146
|
-
*/
|
|
147
|
-
export function windowChrome({ tabs, url, urlHost, theme = 'light', w = WIN.w, ext = [] }) {
|
|
148
|
-
const t = THEMES[theme];
|
|
149
|
-
const layout = tabLayout(tabs, w);
|
|
150
|
-
const tabHtml = tabs.map((tab, i) => {
|
|
151
|
-
const { x, width } = layout[i];
|
|
152
|
-
const sepAfter = i < tabs.length - 1 && !tab.active && !tabs[i + 1].active;
|
|
153
|
-
const alert = tab.muted ? icon('muted', 16) : tab.audible ? icon('sound', 16) : '';
|
|
154
|
-
const showClose = tab.active || width >= 120;
|
|
155
|
-
return `<div class="tab${tab.active ? ' active' : ''}" style="left:${x}px;width:${width}px">
|
|
156
|
-
<img class="fav" src="${tab.icon}" width="16" height="16" alt="">
|
|
157
|
-
<span class="title">${esc(tab.title)}</span>
|
|
158
|
-
${alert ? `<span class="alert">${alert}</span>` : ''}
|
|
159
|
-
${showClose ? `<span class="x">${icon('close', 14)}</span>` : ''}
|
|
160
|
-
</div>${sepAfter ? `<span class="sep" style="left:${x + width - 1}px"></span>` : ''}`;
|
|
161
|
-
}).join('');
|
|
162
|
-
const last = layout[layout.length - 1];
|
|
163
|
-
const plusX = last.x + last.width + 4;
|
|
164
|
-
const extIcons = ext.map((src) => `<img src="${src}" width="16" height="16" alt="" style="margin:0 9px">`).join('');
|
|
165
|
-
return `
|
|
166
|
-
<div class="strip">
|
|
167
|
-
<span class="lights"><i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i></span>
|
|
168
|
-
${tabHtml}
|
|
169
|
-
<span class="plus" style="left:${plusX}px">${icon('add', 18)}</span>
|
|
170
|
-
</div>
|
|
171
|
-
<div class="toolbar">
|
|
172
|
-
<span class="tb">${icon('back')}</span><span class="tb dim">${icon('forward')}</span><span class="tb">${icon('reload')}</span>
|
|
173
|
-
<div class="omni"><span class="site">${icon('tune', 16)}</span><span class="url">${urlHost ? `${esc(urlHost)}<span class="dim">${esc(url.slice(urlHost.length))}</span>` : esc(url)}</span><span class="star">${icon('star', 18)}</span></div>
|
|
174
|
-
${extIcons}<span class="tb">${icon('puzzle')}</span>
|
|
175
|
-
<span class="avatar">${icon('person', 18)}</span><span class="tb">${icon('more')}</span>
|
|
176
|
-
</div>
|
|
177
|
-
<style>
|
|
178
|
-
.strip { position: relative; height: ${WIN.strip}px; background: ${t.frame}; }
|
|
179
|
-
.lights { position: absolute; left: 16px; top: 15px; display: flex; gap: 8px; }
|
|
180
|
-
.lights i { width: 12px; height: 12px; border-radius: 50%; display: block; box-shadow: inset 0 0 0 .5px rgba(0,0,0,.12); }
|
|
181
|
-
.tab { position: absolute; bottom: 0; height: ${TAB_H}px; display: flex; align-items: center; gap: 8px; padding: 0 10px 0 14px; box-sizing: border-box; color: ${t.tabTextInactive}; font: 12px/16px system-ui; }
|
|
182
|
-
.tab.active { background: ${t.activeTab}; color: ${t.tabText}; border-radius: 10px 10px 0 0; z-index: 1; }
|
|
183
|
-
.tab.active::before, .tab.active::after { content: ''; position: absolute; bottom: 0; width: 10px; height: 10px; }
|
|
184
|
-
.tab.active::before { left: -10px; background: radial-gradient(circle at 0 0, transparent 10px, ${t.activeTab} 10.5px); }
|
|
185
|
-
.tab.active::after { right: -10px; background: radial-gradient(circle at 100% 0, transparent 10px, ${t.activeTab} 10.5px); }
|
|
186
|
-
.tab .fav { flex: none; }
|
|
187
|
-
.tab .title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 18px), transparent); }
|
|
188
|
-
.tab .alert, .tab .x { flex: none; display: flex; color: ${t.toolbarIcon}; }
|
|
189
|
-
.sep { position: absolute; bottom: 9px; width: 1px; height: 18px; background: ${t.tabSep}; }
|
|
190
|
-
.plus { position: absolute; top: 11px; display: flex; color: ${t.toolbarIcon}; padding: 2px; }
|
|
191
|
-
.toolbar { height: ${WIN.toolbar}px; background: ${t.toolbar}; display: flex; align-items: center; gap: 4px; padding: 0 8px; box-sizing: border-box; border-bottom: 1px solid ${t.contentLine}; }
|
|
192
|
-
.tb { width: 34px; height: 34px; display: grid; place-items: center; color: ${t.toolbarIcon}; flex: none; }
|
|
193
|
-
.tb.dim { opacity: .45; }
|
|
194
|
-
.omni { flex: 1; min-width: 0; height: 34px; margin: 0 8px; border-radius: 17px; background: ${t.omnibox}; display: flex; align-items: center; gap: 10px; padding: 0 12px; color: ${t.url}; font: 14px system-ui; }
|
|
195
|
-
.omni .site { display: flex; color: ${t.toolbarIcon}; }
|
|
196
|
-
.omni .url { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
197
|
-
.omni .dim { color: ${t.urlDim}; }
|
|
198
|
-
.omni .star { display: flex; color: ${t.toolbarIcon}; }
|
|
199
|
-
.avatar { flex: none; width: 26px; height: 26px; margin: 0 4px; border-radius: 50%; background: #7cacf8; color: #fff; display: grid; place-items: end center; overflow: hidden; }
|
|
200
|
-
</style>`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
55
|
/**
|
|
204
56
|
* The macOS arrow pointer.
|
|
205
57
|
*
|
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', 'ai', 'commands', 'content', 'dom', 'scripting', 'settings', 'sites', 'styles', 'time', 'toolbar', 'ui']);
|
|
31
|
+
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'ai', 'commands', 'content', 'dom', 'frame', 'panel', 'scripting', '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');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A content script's layer over the page: a host element of its own name on
|
|
3
|
+
* the page's root element, holding a closed shadow root with the script's
|
|
4
|
+
* styles, so the page's styles and scripts can't reach in and its styles
|
|
5
|
+
* can't reach out. Pickers, toasts, bars and inspectors draw in it.
|
|
6
|
+
*
|
|
7
|
+
* @module content/overlay
|
|
8
|
+
*/
|
|
9
|
+
export type PageOverlay = {
|
|
10
|
+
/**
|
|
11
|
+
* The host element, on the page's root element.
|
|
12
|
+
*/
|
|
13
|
+
host: HTMLElement;
|
|
14
|
+
/**
|
|
15
|
+
* Its closed shadow root, the styles first: append to it.
|
|
16
|
+
*/
|
|
17
|
+
root: ShadowRoot;
|
|
18
|
+
/**
|
|
19
|
+
* Take it off the page.
|
|
20
|
+
*/
|
|
21
|
+
remove: () => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A page overlay.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {object} PageOverlay
|
|
27
|
+
* @property {HTMLElement} host The host element, on the page's root element.
|
|
28
|
+
* @property {ShadowRoot} root Its closed shadow root, the styles first: append to it.
|
|
29
|
+
* @property {() => void} remove Take it off the page.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Put an overlay on the page, replacing one of the same name a run before left.
|
|
33
|
+
* The host's own styles are reset (`all: initial`) before `css`, so the page's
|
|
34
|
+
* rules for unknown elements can't move or hide it; `css` styles `:host` and what's inside.
|
|
35
|
+
*
|
|
36
|
+
* @param {Document} doc - The page.
|
|
37
|
+
* @param {{name: string, css: string}} options - `name`: the host's element name, with a hyphen
|
|
38
|
+
* (never registered: a plain element); `css`: the overlay's styles.
|
|
39
|
+
* @returns {PageOverlay} The overlay.
|
|
40
|
+
* @example
|
|
41
|
+
* const { root, remove } = pageOverlay(document, { name: 'better-fonts-inspector', css: ':host { position: fixed; inset: 0; pointer-events: none; }' });
|
|
42
|
+
* root.append(card);
|
|
43
|
+
*/
|
|
44
|
+
export declare function pageOverlay(doc: Document, { name, css }: {
|
|
45
|
+
name: string;
|
|
46
|
+
css: string;
|
|
47
|
+
}): PageOverlay;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A content script's layer over the page: a host element of its own name on
|
|
3
|
+
* the page's root element, holding a closed shadow root with the script's
|
|
4
|
+
* styles, so the page's styles and scripts can't reach in and its styles
|
|
5
|
+
* can't reach out. Pickers, toasts, bars and inspectors draw in it.
|
|
6
|
+
*
|
|
7
|
+
* @module content/overlay
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A page overlay.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {object} PageOverlay
|
|
14
|
+
* @property {HTMLElement} host The host element, on the page's root element.
|
|
15
|
+
* @property {ShadowRoot} root Its closed shadow root, the styles first: append to it.
|
|
16
|
+
* @property {() => void} remove Take it off the page.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Put an overlay on the page, replacing one of the same name a run before left.
|
|
21
|
+
* The host's own styles are reset (`all: initial`) before `css`, so the page's
|
|
22
|
+
* rules for unknown elements can't move or hide it; `css` styles `:host` and what's inside.
|
|
23
|
+
*
|
|
24
|
+
* @param {Document} doc - The page.
|
|
25
|
+
* @param {{name: string, css: string}} options - `name`: the host's element name, with a hyphen
|
|
26
|
+
* (never registered: a plain element); `css`: the overlay's styles.
|
|
27
|
+
* @returns {PageOverlay} The overlay.
|
|
28
|
+
* @example
|
|
29
|
+
* const { root, remove } = pageOverlay(document, { name: 'better-fonts-inspector', css: ':host { position: fixed; inset: 0; pointer-events: none; }' });
|
|
30
|
+
* root.append(card);
|
|
31
|
+
*/
|
|
32
|
+
export function pageOverlay(doc, { name, css }) {
|
|
33
|
+
for (const old of doc.querySelectorAll(name)) old.remove();
|
|
34
|
+
const host = doc.createElement(name);
|
|
35
|
+
host.style.setProperty('all', 'initial', 'important');
|
|
36
|
+
const root = host.attachShadow({ mode: 'closed' });
|
|
37
|
+
const style = doc.createElement('style');
|
|
38
|
+
style.textContent = css;
|
|
39
|
+
root.append(style);
|
|
40
|
+
doc.documentElement.append(host);
|
|
41
|
+
return { host, root, remove: () => host.remove() };
|
|
42
|
+
}
|
package/src/content/slices.d.ts
CHANGED
|
@@ -48,6 +48,21 @@ export type SliceOptions = {
|
|
|
48
48
|
* @property {() => Promise<void>} [pause] Hands the main thread back. Default: a macrotask.
|
|
49
49
|
* @property {() => number} [now] The clock. Default `performance.now`.
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* Work through any list in slices: `work(item, index)` on each, the main
|
|
53
|
+
* thread handed back whenever a slice has run `sliceMs`. For a content
|
|
54
|
+
* script's work that isn't cut by blocks of the page (each element's style
|
|
55
|
+
* read, each text measured).
|
|
56
|
+
*
|
|
57
|
+
* @template T
|
|
58
|
+
* @param {Iterable<T>} items - The items.
|
|
59
|
+
* @param {(item: T, index: number) => void} work - What to do with one.
|
|
60
|
+
* @param {Pick<SliceOptions, 'sliceMs' | 'pause' | 'now'>} [options] - Slice length, how to pause, the clock.
|
|
61
|
+
* @returns {Promise<void>} Once every item is done.
|
|
62
|
+
* @example
|
|
63
|
+
* await eachInSlices(elements, (element) => styles.push(getComputedStyle(element).fontFamily));
|
|
64
|
+
*/
|
|
65
|
+
export declare function eachInSlices<T>(items: Iterable<T>, work: (item: T, index: number) => void, { sliceMs, pause, now }?: Pick<SliceOptions, 'sliceMs' | 'pause' | 'now'>): Promise<void>;
|
|
51
66
|
/**
|
|
52
67
|
* Work through a part of the page in slices: `work(block)` on each block
|
|
53
68
|
* still in the page, the main thread handed back whenever a slice has run
|
|
@@ -60,4 +75,4 @@ export type SliceOptions = {
|
|
|
60
75
|
* @example
|
|
61
76
|
* await inSlices(document.body, (block) => scanner.scan(block));
|
|
62
77
|
*/
|
|
63
|
-
export declare function inSlices(root: Node, work: (block: Node) => void, { blockChars,
|
|
78
|
+
export declare function inSlices(root: Node, work: (block: Node) => void, { blockChars, ...options }?: SliceOptions): Promise<void>;
|
package/src/content/slices.js
CHANGED
|
@@ -55,6 +55,32 @@ const nextTask = () => new Promise((resolve) => { setTimeout(resolve, 0); });
|
|
|
55
55
|
* @property {() => number} [now] The clock. Default `performance.now`.
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Work through any list in slices: `work(item, index)` on each, the main
|
|
60
|
+
* thread handed back whenever a slice has run `sliceMs`. For a content
|
|
61
|
+
* script's work that isn't cut by blocks of the page (each element's style
|
|
62
|
+
* read, each text measured).
|
|
63
|
+
*
|
|
64
|
+
* @template T
|
|
65
|
+
* @param {Iterable<T>} items - The items.
|
|
66
|
+
* @param {(item: T, index: number) => void} work - What to do with one.
|
|
67
|
+
* @param {Pick<SliceOptions, 'sliceMs' | 'pause' | 'now'>} [options] - Slice length, how to pause, the clock.
|
|
68
|
+
* @returns {Promise<void>} Once every item is done.
|
|
69
|
+
* @example
|
|
70
|
+
* await eachInSlices(elements, (element) => styles.push(getComputedStyle(element).fontFamily));
|
|
71
|
+
*/
|
|
72
|
+
export async function eachInSlices(items, work, { sliceMs = SLICE_MS, pause = nextTask, now = () => performance.now() } = {}) {
|
|
73
|
+
let started = now();
|
|
74
|
+
let index = 0;
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
work(item, index++);
|
|
77
|
+
if (now() - started > sliceMs) {
|
|
78
|
+
await pause();
|
|
79
|
+
started = now();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
/**
|
|
59
85
|
* Work through a part of the page in slices: `work(block)` on each block
|
|
60
86
|
* still in the page, the main thread handed back whenever a slice has run
|
|
@@ -67,14 +93,7 @@ const nextTask = () => new Promise((resolve) => { setTimeout(resolve, 0); });
|
|
|
67
93
|
* @example
|
|
68
94
|
* await inSlices(document.body, (block) => scanner.scan(block));
|
|
69
95
|
*/
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!block.isConnected) continue;
|
|
74
|
-
work(block);
|
|
75
|
-
if (now() - started > sliceMs) {
|
|
76
|
-
await pause();
|
|
77
|
-
started = now();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
96
|
+
export function inSlices(root, work, { blockChars = BLOCK_CHARS, ...options } = {}) {
|
|
97
|
+
// A block the work before took out of the page is skipped.
|
|
98
|
+
return eachInSlices(blocksOf(root, blockChars), (block) => { if (block.isConnected) work(block); }, options);
|
|
80
99
|
}
|