sourcemonkey 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// In-memory index of every injectable userscript's matching info so the
|
|
2
|
+
// background service worker can find matching scripts for a URL without
|
|
3
|
+
// touching chrome.storage.local on every navigation. "Injectable" means
|
|
4
|
+
// `status: 'ok'` (see toEntry).
|
|
5
|
+
//
|
|
6
|
+
// Built once on first access (ensureLoaded()), then kept fresh by a
|
|
7
|
+
// chrome.storage.local.onChanged listener. Storage I/O cost scales with
|
|
8
|
+
// the user's script count at SW startup; per-navigation cost is just an
|
|
9
|
+
// in-memory loop over the cache.
|
|
10
|
+
//
|
|
11
|
+
// Enabled state: a script is considered for matching only if BOTH its
|
|
12
|
+
// own `enabled` flag and its parent collection's `enabled` flag are
|
|
13
|
+
// true. We cache every injectable script (regardless of enabled) and
|
|
14
|
+
// separately track the set of disabled collections, so toggling a
|
|
15
|
+
// collection flips every script under it in O(1) without rewalking
|
|
16
|
+
// storage.
|
|
17
|
+
import { isFileUrl, scratchRunOf } from './directories.js';
|
|
18
|
+
import { compareScriptNames, scriptDisplayName } from '../script-display.js';
|
|
19
|
+
import { scanStorageRows } from './storage-scan.js';
|
|
20
|
+
import { sharedRows } from './row-cache.js';
|
|
21
|
+
import { ensureCollectionKeysMigrated, isScriptEnabled, isCollectionEnabled, sameStoredValue, withoutAnnotations, } from './script-store.js';
|
|
22
|
+
import { urlMatchesScript } from '../url-match.js';
|
|
23
|
+
import { resolveScriptWorld } from '../script-world.js';
|
|
24
|
+
import { grantedSubsystems } from '../gm-api-catalog.js';
|
|
25
|
+
import { isManualScript } from '../script-build.js';
|
|
26
|
+
import { hasTag } from '../userscript-header.js';
|
|
27
|
+
const SCRIPT_PREFIX = 'script:';
|
|
28
|
+
const COLLECTION_PREFIX = 'collection:';
|
|
29
|
+
let cache = new Map();
|
|
30
|
+
// The match rules of `invalid` scripts, which `cache` leaves out. Read
|
|
31
|
+
// only by `findAllMatchingIds`, so the popup can list a script that
|
|
32
|
+
// targets the page but failed to load. Never injected or registered.
|
|
33
|
+
let invalidMatching = new Map();
|
|
34
|
+
let disabledCollections = new Set();
|
|
35
|
+
let loaded = null;
|
|
36
|
+
// While loadAll() is awaiting its full `get(null)` snapshot, any
|
|
37
|
+
// storage change that fires would mutate the about-to-be-replaced
|
|
38
|
+
// `cache`, and the subsequent `cache = nextCache` assignment would
|
|
39
|
+
// discard it — a lost update until the next change to that key. During
|
|
40
|
+
// the load window we buffer raw changes here instead of applying them,
|
|
41
|
+
// then replay them onto the freshly-built cache. Null when not loading.
|
|
42
|
+
let pendingDuringLoad = null;
|
|
43
|
+
// First call kicks off a single full read; later callers await the same
|
|
44
|
+
// promise. After it resolves, the cache is kept fresh by the onChanged
|
|
45
|
+
// listener installed by installStorageListener().
|
|
46
|
+
export function ensureLoaded() {
|
|
47
|
+
if (!loaded)
|
|
48
|
+
loaded = loadAll();
|
|
49
|
+
return loaded;
|
|
50
|
+
}
|
|
51
|
+
async function loadAll() {
|
|
52
|
+
pendingDuringLoad = [];
|
|
53
|
+
// Before the read, not after: a profile still on the pre-rename key
|
|
54
|
+
// spelling would otherwise load an empty cache and inject nothing.
|
|
55
|
+
await ensureCollectionKeysMigrated();
|
|
56
|
+
const isRow = (k) => k.startsWith(SCRIPT_PREFIX) || k.startsWith(COLLECTION_PREFIX);
|
|
57
|
+
// From the worker's row cache when it's on (frozen rows, read only).
|
|
58
|
+
const all = (await sharedRows(isRow)) ?? await scanStorageRows('match index', isRow);
|
|
59
|
+
const nextCache = new Map();
|
|
60
|
+
const nextInvalid = new Map();
|
|
61
|
+
const nextDisabled = new Set();
|
|
62
|
+
for (const [key, value] of Object.entries(all)) {
|
|
63
|
+
if (key.startsWith(SCRIPT_PREFIX)) {
|
|
64
|
+
const script = value;
|
|
65
|
+
const entry = toEntry(script);
|
|
66
|
+
if (entry)
|
|
67
|
+
nextCache.set(entry.id, entry);
|
|
68
|
+
else if (script.status === 'invalid' && script.matching)
|
|
69
|
+
nextInvalid.set(script.id, script.matching);
|
|
70
|
+
}
|
|
71
|
+
else if (key.startsWith(COLLECTION_PREFIX)) {
|
|
72
|
+
const collection = value;
|
|
73
|
+
if (!isCollectionEnabled(collection))
|
|
74
|
+
nextDisabled.add(collection.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cache = nextCache;
|
|
78
|
+
invalidMatching = nextInvalid;
|
|
79
|
+
disabledCollections = nextDisabled;
|
|
80
|
+
// Replay any changes that landed during the await on top of the
|
|
81
|
+
// fresh snapshot, then stop buffering. A change for a key already in
|
|
82
|
+
// the snapshot just re-applies the same (or newer) value; one for a
|
|
83
|
+
// key the snapshot missed is now captured.
|
|
84
|
+
const buffered = pendingDuringLoad;
|
|
85
|
+
pendingDuringLoad = null;
|
|
86
|
+
for (const changes of buffered)
|
|
87
|
+
applyStorageChanges(changes);
|
|
88
|
+
}
|
|
89
|
+
function toEntry(script) {
|
|
90
|
+
// Only `ok` scripts participate. `invalid` rows never do — their
|
|
91
|
+
// header may be unparsed or the file failed validation.
|
|
92
|
+
if (script.status !== 'ok')
|
|
93
|
+
return null;
|
|
94
|
+
const name = scriptDisplayName(script);
|
|
95
|
+
return {
|
|
96
|
+
id: script.id,
|
|
97
|
+
name,
|
|
98
|
+
matching: script.matching,
|
|
99
|
+
collectionId: script.collectionId,
|
|
100
|
+
world: resolveScriptWorld(script),
|
|
101
|
+
subsystems: grantedSubsystems(script.header),
|
|
102
|
+
enabled: isScriptEnabled(script),
|
|
103
|
+
local: isFileUrl(script.fileUrl),
|
|
104
|
+
...smDevRunField(script.fileUrl),
|
|
105
|
+
...(isManualScript(script) ? { manual: true } : {}),
|
|
106
|
+
...(script.header && hasTag(script.header, 'noframes') ? { noframes: true } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function smDevRunField(fileUrl) {
|
|
110
|
+
const run = scratchRunOf(fileUrl);
|
|
111
|
+
return run === null ? {} : { smDevRun: run };
|
|
112
|
+
}
|
|
113
|
+
// Install the storage listener exactly once. Safe to call repeatedly.
|
|
114
|
+
let listenerInstalled = false;
|
|
115
|
+
export function installStorageListener() {
|
|
116
|
+
if (listenerInstalled)
|
|
117
|
+
return;
|
|
118
|
+
listenerInstalled = true;
|
|
119
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
120
|
+
if (areaName !== 'local')
|
|
121
|
+
return;
|
|
122
|
+
// If a full load is mid-flight, buffer this change for replay after
|
|
123
|
+
// the snapshot lands rather than mutating the soon-to-be-replaced
|
|
124
|
+
// cache (which would lose this update). See loadAll().
|
|
125
|
+
if (pendingDuringLoad) {
|
|
126
|
+
pendingDuringLoad.push(changes);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
applyStorageChanges(changes);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Apply one storage.onChanged delta to the in-memory cache. Shared by
|
|
133
|
+
// the live listener and loadAll()'s post-snapshot replay.
|
|
134
|
+
function applyStorageChanges(changes) {
|
|
135
|
+
// Which rows moved, not just "something moved". The injector syncs
|
|
136
|
+
// incrementally off this, so a listener that only said "changed"
|
|
137
|
+
// would force it back to rebuilding every registration.
|
|
138
|
+
const scriptIds = [];
|
|
139
|
+
const collectionIds = [];
|
|
140
|
+
for (const [key, change] of Object.entries(changes)) {
|
|
141
|
+
if (key.startsWith(SCRIPT_PREFIX)) {
|
|
142
|
+
// A write that only moved an annotation (`versionProbes`, or the
|
|
143
|
+
// parse check's verdict) is not a change to anything downstream
|
|
144
|
+
// of this listener, and saying otherwise is expensive: each one
|
|
145
|
+
// schedules a registration sync. A "Refresh All" probes each
|
|
146
|
+
// script in turn and writes each result as its own row, so
|
|
147
|
+
// treating those as cache changes turned one refresh into a
|
|
148
|
+
// re-registration per probed script — measured at 23 syncs
|
|
149
|
+
// averaging 356ms for a 177-script profile, most of them landing
|
|
150
|
+
// after the refresh had already finished. A verdict cost a second
|
|
151
|
+
// sync after every edit.
|
|
152
|
+
//
|
|
153
|
+
// Safe for a probe because it is a display annotation and nothing
|
|
154
|
+
// else: `toEntry` doesn't read it, and neither does `injector.ts`
|
|
155
|
+
// (registration specs), `tab-status.ts`, or `background.ts`. The
|
|
156
|
+
// pages that DO render it — the viewer and the collections page —
|
|
157
|
+
// repaint from their own `chrome.storage.local.onChanged`
|
|
158
|
+
// listeners, not from `onCacheChanged`, so the badge still
|
|
159
|
+
// updates the moment a probe lands.
|
|
160
|
+
//
|
|
161
|
+
// Safe for a passing verdict for another reason: the sync reads
|
|
162
|
+
// it (its gate checks rows without one), but it lands just after
|
|
163
|
+
// the content change it checked, whose own write started a sync
|
|
164
|
+
// then, and the registration built from the row is the same
|
|
165
|
+
// either way. A failing one changes `status`, which is reported.
|
|
166
|
+
if (onlyAnnotationsChanged(change.oldValue, change.newValue)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const id = key.slice(SCRIPT_PREFIX.length);
|
|
170
|
+
scriptIds.push(id);
|
|
171
|
+
invalidMatching.delete(id);
|
|
172
|
+
if (change.newValue === undefined) {
|
|
173
|
+
cache.delete(id);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const script = change.newValue;
|
|
177
|
+
const entry = toEntry(script);
|
|
178
|
+
if (entry)
|
|
179
|
+
cache.set(id, entry);
|
|
180
|
+
else
|
|
181
|
+
cache.delete(id);
|
|
182
|
+
if (script.status === 'invalid' && script.matching)
|
|
183
|
+
invalidMatching.set(id, script.matching);
|
|
184
|
+
}
|
|
185
|
+
else if (key.startsWith(COLLECTION_PREFIX)) {
|
|
186
|
+
const id = key.slice(COLLECTION_PREFIX.length);
|
|
187
|
+
const next = change.newValue;
|
|
188
|
+
// A collection row is rewritten on every scan (`lastScannedAt`
|
|
189
|
+
// and friends), so reporting every write would re-register every
|
|
190
|
+
// script under it on every refresh. Only what moves anything
|
|
191
|
+
// downstream is reported: the enabled flag, and the row going
|
|
192
|
+
// away.
|
|
193
|
+
//
|
|
194
|
+
// Deliberately NOT its arrival. A collection appears together
|
|
195
|
+
// with the scripts under it, which are reported in their own
|
|
196
|
+
// right, and the injector fetches a collection row it hasn't seen
|
|
197
|
+
// when a script names one — so saying so here would buy nothing
|
|
198
|
+
// and cost an extra sync per collection added.
|
|
199
|
+
const wasDisabled = disabledCollections.has(id);
|
|
200
|
+
if (next === undefined) {
|
|
201
|
+
// Collection was removed entirely — its scripts will be deleted
|
|
202
|
+
// via separate script:* changes; just drop our tracking row.
|
|
203
|
+
disabledCollections.delete(id);
|
|
204
|
+
collectionIds.push(id);
|
|
205
|
+
}
|
|
206
|
+
else if (!isCollectionEnabled(next)) {
|
|
207
|
+
disabledCollections.add(id);
|
|
208
|
+
if (!wasDisabled)
|
|
209
|
+
collectionIds.push(id);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
disabledCollections.delete(id);
|
|
213
|
+
if (wasDisabled)
|
|
214
|
+
collectionIds.push(id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (scriptIds.length > 0 || collectionIds.length > 0) {
|
|
219
|
+
notifyChange({ scriptIds, collectionIds });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Whether a `script:*` write moved nothing but its annotations.
|
|
223
|
+
//
|
|
224
|
+
// Deliberately a diff of the two stored rows rather than of the derived
|
|
225
|
+
// cache entries. The entry holds only what MATCHING needs, but
|
|
226
|
+
// `notifyChange` also drives re-registration, which reads the whole row
|
|
227
|
+
// — body, `@require` bodies, grants, world, run-at. Comparing entries
|
|
228
|
+
// would call a body-only edit "no change" and leave the old code
|
|
229
|
+
// registered, which is a correctness bug, not a missed optimisation.
|
|
230
|
+
// So the rule is inverted: assume a change matters unless it is exactly
|
|
231
|
+
// the fields proven not to.
|
|
232
|
+
//
|
|
233
|
+
// Exported for unit tests.
|
|
234
|
+
export function onlyAnnotationsChanged(before, after) {
|
|
235
|
+
// A creation or a deletion is never "just an annotation".
|
|
236
|
+
if (!before || !after)
|
|
237
|
+
return false;
|
|
238
|
+
return sameStoredValue(withoutAnnotations(before), withoutAnnotations(after));
|
|
239
|
+
}
|
|
240
|
+
const changeListeners = [];
|
|
241
|
+
export function onCacheChanged(fn) {
|
|
242
|
+
changeListeners.push(fn);
|
|
243
|
+
}
|
|
244
|
+
function notifyChange(change) {
|
|
245
|
+
for (const fn of changeListeners) {
|
|
246
|
+
try {
|
|
247
|
+
fn(change);
|
|
248
|
+
}
|
|
249
|
+
catch { /* swallow listener errors */ }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export function findMatchesForUrl(url, kind = 'load') {
|
|
253
|
+
const out = [];
|
|
254
|
+
for (const entry of cache.values()) {
|
|
255
|
+
if (!entry.enabled)
|
|
256
|
+
continue;
|
|
257
|
+
if (disabledCollections.has(entry.collectionId))
|
|
258
|
+
continue;
|
|
259
|
+
if (kind === 'load' && entry.manual)
|
|
260
|
+
continue;
|
|
261
|
+
if (kind === 'manual' && !entry.manual)
|
|
262
|
+
continue;
|
|
263
|
+
if (urlMatchesScript(url, entry.matching)) {
|
|
264
|
+
out.push({
|
|
265
|
+
id: entry.id,
|
|
266
|
+
name: entry.name,
|
|
267
|
+
matching: entry.matching,
|
|
268
|
+
collectionId: entry.collectionId,
|
|
269
|
+
world: entry.world,
|
|
270
|
+
...(entry.smDevRun !== undefined ? { smDevRun: entry.smDevRun } : {}),
|
|
271
|
+
...(entry.manual ? { manual: true } : {}),
|
|
272
|
+
...(entry.noframes ? { noframes: true } : {}),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Stable display order for the tooltip.
|
|
277
|
+
out.sort((a, b) => compareScriptNames(a.name, b.name));
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
// What the index knows of script `id` (any `ok` script, enabled or
|
|
281
|
+
// not), or undefined for one it doesn't hold.
|
|
282
|
+
export function indexedScript(id) {
|
|
283
|
+
const entry = cache.get(id);
|
|
284
|
+
return entry && {
|
|
285
|
+
world: entry.world,
|
|
286
|
+
...(entry.noframes ? { noframes: true } : {}),
|
|
287
|
+
...(entry.manual ? { manual: true } : {}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// Whether `id` is a manual script (`@run-at context-menu`). False for
|
|
291
|
+
// an id the index doesn't hold.
|
|
292
|
+
export function isManualScriptId(id) {
|
|
293
|
+
return cache.get(id)?.manual === true;
|
|
294
|
+
}
|
|
295
|
+
// Every script whose rules match `url`, enabled or not — the popup
|
|
296
|
+
// lists the disabled ones too, so they can be turned on from there.
|
|
297
|
+
// `invalid` ones as well: one that targets the page but failed to load
|
|
298
|
+
// is listed as such. Ids only: the popup reads the rows it needs from
|
|
299
|
+
// storage.
|
|
300
|
+
export function findAllMatchingIds(url) {
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const entry of cache.values()) {
|
|
303
|
+
if (urlMatchesScript(url, entry.matching))
|
|
304
|
+
out.push(entry.id);
|
|
305
|
+
}
|
|
306
|
+
for (const [id, matching] of invalidMatching) {
|
|
307
|
+
if (urlMatchesScript(url, matching))
|
|
308
|
+
out.push(id);
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
// Narrow a set of script ids to the ones loaded from `file://`.
|
|
313
|
+
//
|
|
314
|
+
// Keyed on each script's own `fileUrl`, not on its collection's
|
|
315
|
+
// location: a container collection holds individually-installed scripts
|
|
316
|
+
// of both kinds, so the collection is the wrong unit for this question.
|
|
317
|
+
// (`isLocalCollection` in refresh-targets.ts answers the collection-level
|
|
318
|
+
// version, and has to special-case containers for exactly this reason.)
|
|
319
|
+
//
|
|
320
|
+
// Ids the cache doesn't hold — a script that isn't injectable, or one
|
|
321
|
+
// deleted since the tab last matched — are dropped rather than assumed
|
|
322
|
+
// local: there is nothing to refresh either way.
|
|
323
|
+
export function localScriptIds(ids) {
|
|
324
|
+
return ids.filter((id) => cache.get(id)?.local === true);
|
|
325
|
+
}
|
|
326
|
+
// Does the profile hold ANY injectable local script?
|
|
327
|
+
//
|
|
328
|
+
// The cheap gate in front of the focus-triggered refresh: it runs on
|
|
329
|
+
// every tab switch and window focus change, and a profile with only
|
|
330
|
+
// http collections has nothing for it to do. Answering from the
|
|
331
|
+
// in-memory cache keeps that case off `chrome.storage.session`.
|
|
332
|
+
export function hasLocalScripts() {
|
|
333
|
+
for (const entry of cache.values())
|
|
334
|
+
if (entry.local)
|
|
335
|
+
return true;
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
export function cacheSize() {
|
|
339
|
+
return cache.size;
|
|
340
|
+
}
|
|
341
|
+
// The execution world a script would be injected into, or null when the
|
|
342
|
+
// id isn't in the injectable cache at all.
|
|
343
|
+
//
|
|
344
|
+
// Used by the SW to stamp every push it sends into a tab with the world
|
|
345
|
+
// the receiving script lives in. `wrapper-bridge.js` re-posts pushes
|
|
346
|
+
// into MAIN world, where the page can read them, so it must relay only
|
|
347
|
+
// what is addressed to a MAIN-world script — otherwise an isolated
|
|
348
|
+
// script's stored values and XHR response bodies would become
|
|
349
|
+
// page-visible on any page that happens to also run a MAIN-world
|
|
350
|
+
// script. Callers treat `null` as "not MAIN" (fail closed).
|
|
351
|
+
export function scriptWorld(id) {
|
|
352
|
+
return cache.get(id)?.world ?? null;
|
|
353
|
+
}
|
|
354
|
+
// True iff the script's `@grant` lines reach one of the given
|
|
355
|
+
// subsystems. The second half of the bridge gate: being a MAIN-world
|
|
356
|
+
// script on the page only makes the APIs it granted page-reachable, so
|
|
357
|
+
// a request through any other API is refused even for that script.
|
|
358
|
+
// Unknown ids fail closed.
|
|
359
|
+
export function scriptGrantsSubsystem(id, subsystems) {
|
|
360
|
+
const granted = cache.get(id)?.subsystems;
|
|
361
|
+
if (!granted)
|
|
362
|
+
return false;
|
|
363
|
+
return subsystems.some((s) => granted.has(s));
|
|
364
|
+
}
|
|
365
|
+
// Test/debug: dump the current cache contents. Lets e2e tests
|
|
366
|
+
// distinguish "the script never landed in the cache" from "the URL
|
|
367
|
+
// pattern doesn't match" when a badge classification looks wrong.
|
|
368
|
+
export function debugDumpCache() {
|
|
369
|
+
return {
|
|
370
|
+
entries: [...cache.values()].map((e) => ({
|
|
371
|
+
id: e.id, name: e.name, enabled: e.enabled, collectionId: e.collectionId, matching: e.matching,
|
|
372
|
+
})),
|
|
373
|
+
disabledCollections: [...disabledCollections],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Explaining a script that matched a page but never started there.
|
|
2
|
+
//
|
|
3
|
+
// The badge shows such a script as MISSING. Once the page has loaded,
|
|
4
|
+
// the service worker also writes a Log warning for it, built here from
|
|
5
|
+
// what the document's other wrapper reports said (`reportedWorlds`,
|
|
6
|
+
// `opaqueOrigin` on TabMatchState). The main case it names:
|
|
7
|
+
// - A CSP `sandbox` header without `allow-scripts` turns off script
|
|
8
|
+
// in the page's own world, so Chrome silently skips MAIN-world
|
|
9
|
+
// scripts there, while USER_SCRIPT-world ones still run. The
|
|
10
|
+
// bridge then has them run isolated (`runBlockedMainScripts` in
|
|
11
|
+
// background.ts), so this hint matters only when that didn't run.
|
|
12
|
+
// - Such a page gets an opaque origin, which any wrapper that did run
|
|
13
|
+
// there reports.
|
|
14
|
+
// See docs/matching.md#scripts-that-never-start.
|
|
15
|
+
import { skippedOnDocument } from './tab-status.js';
|
|
16
|
+
import { INJECT_INTO_CONTENT_FIX, SANDBOX_BLOCKS_PAGE_CONTEXT } from '../sandboxed-page.js';
|
|
17
|
+
// The scripts expected on this document from its first load that never
|
|
18
|
+
// reported, and aren't already explained by the badge (REJECTED, NOT
|
|
19
|
+
// LOADED). See `loadMatchIds` for why only the first load's.
|
|
20
|
+
export function findMissingScripts(state, rejected = new Set()) {
|
|
21
|
+
// Nothing was injected, so no script on it is a mystery to explain.
|
|
22
|
+
if (state.scriptsSkipped || state.loadedWithoutUserscripts)
|
|
23
|
+
return [];
|
|
24
|
+
const atLoad = state.loadMatchIds ? new Set(state.loadMatchIds) : null;
|
|
25
|
+
return state.matches.filter((m) => (!atLoad || atLoad.has(m.id)) && !state.actual[m.id] && !rejected.has(m.id) && !skippedOnDocument(state, m.id));
|
|
26
|
+
}
|
|
27
|
+
// Whether the document is sandboxed, going by the wrappers' reports.
|
|
28
|
+
// Only on http(s): a file:// page has an opaque origin too.
|
|
29
|
+
function sandboxed(state) {
|
|
30
|
+
return state.opaqueOrigin === true && /^https?:/i.test(state.url);
|
|
31
|
+
}
|
|
32
|
+
function reportedFrom(state, world) {
|
|
33
|
+
return (state.reportedWorlds ?? []).includes(world);
|
|
34
|
+
}
|
|
35
|
+
// The Log warning for one missing script.
|
|
36
|
+
export function describeMissingScript(script, state) {
|
|
37
|
+
const base = `Matched ${state.url} but never started there`;
|
|
38
|
+
let hint;
|
|
39
|
+
if (script.world === 'MAIN') {
|
|
40
|
+
if (reportedFrom(state, 'MAIN')) {
|
|
41
|
+
hint = 'Other scripts in the page\'s own context did start here, so the page isn\'t blocking that context.';
|
|
42
|
+
}
|
|
43
|
+
else if (sandboxed(state)) {
|
|
44
|
+
hint = 'This page is sandboxed, usually by a sandbox directive in its Content-Security-Policy header. ' +
|
|
45
|
+
`${SANDBOX_BLOCKS_PAGE_CONTEXT}. ${INJECT_INTO_CONTENT_FIX}.`;
|
|
46
|
+
}
|
|
47
|
+
else if (!/^https?:/i.test(state.url)) {
|
|
48
|
+
// No response headers to point at.
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
hint = 'The page may be blocking scripts in its own context. Check its Content-Security-Policy ' +
|
|
53
|
+
`header for a sandbox directive without allow-scripts. If it has one, ${lowerFirst(INJECT_INTO_CONTENT_FIX)}.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (reportedFrom(state, 'USER_SCRIPT')) {
|
|
57
|
+
hint = 'Other scripts in the isolated context did start here.';
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
return base;
|
|
61
|
+
}
|
|
62
|
+
return `${base}. ${hint}`;
|
|
63
|
+
}
|
|
64
|
+
function lowerFirst(text) {
|
|
65
|
+
return text.charAt(0).toLowerCase() + text.slice(1);
|
|
66
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The ⋯ button and the menu it opens, shared by the toolbar popup and
|
|
2
|
+
// the collections page. One menu is open at a time; a click elsewhere
|
|
3
|
+
// or Escape closes it. Each page styles its own `button.more-menu`; the
|
|
4
|
+
// menu's look is `.menu` in styles.css.
|
|
5
|
+
let openMenuEl = null;
|
|
6
|
+
let openMenuBtn = null;
|
|
7
|
+
let listening = false;
|
|
8
|
+
export function moreMenuButton(label) {
|
|
9
|
+
const b = document.createElement('button');
|
|
10
|
+
b.type = 'button';
|
|
11
|
+
b.className = 'more-menu';
|
|
12
|
+
b.setAttribute('aria-label', label);
|
|
13
|
+
b.setAttribute('aria-haspopup', 'menu');
|
|
14
|
+
b.setAttribute('aria-expanded', 'false');
|
|
15
|
+
b.textContent = '⋯';
|
|
16
|
+
return b;
|
|
17
|
+
}
|
|
18
|
+
// Open `items` under `btn`, or close the menu when it is already open
|
|
19
|
+
// there.
|
|
20
|
+
export function openMenu(btn, items) {
|
|
21
|
+
listen();
|
|
22
|
+
const wasOpen = openMenuBtn === btn;
|
|
23
|
+
closeMenu();
|
|
24
|
+
if (wasOpen)
|
|
25
|
+
return;
|
|
26
|
+
const menu = document.createElement('div');
|
|
27
|
+
menu.className = 'menu';
|
|
28
|
+
menu.setAttribute('role', 'menu');
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
if (item.hidden)
|
|
31
|
+
continue;
|
|
32
|
+
if (item.separator) {
|
|
33
|
+
menu.append(document.createElement('hr'));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const b = document.createElement('button');
|
|
37
|
+
b.type = 'button';
|
|
38
|
+
b.setAttribute('role', 'menuitem');
|
|
39
|
+
b.textContent = item.label ?? '';
|
|
40
|
+
b.disabled = item.disabled === true;
|
|
41
|
+
if (item.title)
|
|
42
|
+
b.title = item.title;
|
|
43
|
+
if (item.wire) {
|
|
44
|
+
item.wire(b, closeMenu);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
b.addEventListener('click', () => {
|
|
48
|
+
closeMenu();
|
|
49
|
+
if (!item.run)
|
|
50
|
+
return;
|
|
51
|
+
// Called inside the click, so the action keeps its user gesture.
|
|
52
|
+
try {
|
|
53
|
+
Promise.resolve(item.run()).catch(menuActionFailed);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
menuActionFailed(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
menu.append(b);
|
|
61
|
+
}
|
|
62
|
+
document.body.append(menu);
|
|
63
|
+
// Under the button, right-aligned to it; above it when there's no
|
|
64
|
+
// room below.
|
|
65
|
+
const r = btn.getBoundingClientRect();
|
|
66
|
+
const below = r.bottom + 2;
|
|
67
|
+
const top = below + menu.offsetHeight > document.documentElement.clientHeight
|
|
68
|
+
? Math.max(0, r.top - menu.offsetHeight - 2) : below;
|
|
69
|
+
menu.style.top = `${top}px`;
|
|
70
|
+
menu.style.right = `${document.documentElement.clientWidth - r.right}px`;
|
|
71
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
72
|
+
openMenuEl = menu;
|
|
73
|
+
openMenuBtn = btn;
|
|
74
|
+
menu.querySelector('button')?.focus();
|
|
75
|
+
}
|
|
76
|
+
// Close the menu when the button it hangs from has left the page (a
|
|
77
|
+
// repaint replaced it), rather than leave it floating where it was.
|
|
78
|
+
export function closeDetachedMenu() {
|
|
79
|
+
if (openMenuBtn && !openMenuBtn.isConnected)
|
|
80
|
+
closeMenu();
|
|
81
|
+
}
|
|
82
|
+
export function closeMenu() {
|
|
83
|
+
openMenuEl?.remove();
|
|
84
|
+
openMenuBtn?.setAttribute('aria-expanded', 'false');
|
|
85
|
+
openMenuEl = null;
|
|
86
|
+
openMenuBtn = null;
|
|
87
|
+
}
|
|
88
|
+
function menuActionFailed(err) {
|
|
89
|
+
console.info('[SourceMonkey] menu action failed:', err);
|
|
90
|
+
}
|
|
91
|
+
function listen() {
|
|
92
|
+
if (listening)
|
|
93
|
+
return;
|
|
94
|
+
listening = true;
|
|
95
|
+
document.addEventListener('click', (ev) => {
|
|
96
|
+
const t = ev.target;
|
|
97
|
+
if (!openMenuEl || t?.closest('.menu') || t?.closest('button.more-menu') === openMenuBtn)
|
|
98
|
+
return;
|
|
99
|
+
closeMenu();
|
|
100
|
+
});
|
|
101
|
+
document.addEventListener('keydown', (ev) => {
|
|
102
|
+
if (ev.key === 'Escape' && openMenuEl) {
|
|
103
|
+
const btn = openMenuBtn;
|
|
104
|
+
closeMenu();
|
|
105
|
+
btn?.focus();
|
|
106
|
+
ev.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|