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,703 @@
|
|
|
1
|
+
// The toolbar popup, "Active scripts" (popup.html): the scripts that
|
|
2
|
+
// match the current tab, grouped by collection, with toggles, statuses,
|
|
3
|
+
// menu commands and per-script actions. The service worker builds the
|
|
4
|
+
// view (src/extension/popup-view.ts, `popupViewForTab` in background.ts);
|
|
5
|
+
// this page renders it and sends back what the person clicks. Design:
|
|
6
|
+
// docs/status-popup.md.
|
|
7
|
+
//
|
|
8
|
+
// Opened by the worker with `action.openPopup`, it shows the active tab
|
|
9
|
+
// of its own window. `?tab=<id>` names a tab instead, so tests can open
|
|
10
|
+
// the page in an ordinary tab; `&close=1` then has it close itself as
|
|
11
|
+
// a popup would.
|
|
12
|
+
import { COLLECTION_OFF_LABEL, COLLECTION_OFF_TOOLTIP, POPUP_CHANGED_MESSAGE, POPUP_CLOSE_MESSAGE, POPUP_CLOSING_MESSAGE, POPUP_COMMAND_MESSAGE, POPUP_NO_SCRIPTS, POPUP_RUN_SCRIPT_MESSAGE, RUN_SCRIPT_LABEL, POPUP_NOT_INJECTABLE, POPUP_PORT_PREFIX, POPUP_RELOAD_MESSAGE, POPUP_SET_ENABLED_MESSAGE, POPUP_SET_COMMANDS_OPEN_MESSAGE, POPUP_STATUS_LABELS, POPUP_VIEW_MESSAGE, RELOAD_NO_SCRIPTS_LABEL, REMOVED_GROUP_ID, } from './popup-view.js';
|
|
13
|
+
import { SM_DEV_CONTROL_LABEL } from './tab-status.js';
|
|
14
|
+
import { closeMenu, moreMenuButton, openMenu as openSharedMenu } from './more-menu.js';
|
|
15
|
+
import { collectionAnchorId, EDIT_COLLECTION_HASH_PREFIX, makeCollectionTypeBubble } from './collection-display.js';
|
|
16
|
+
import { namedPageUrl, openBackgroundTab, openExtensionTab, openExtensionWindow, openNamedTab, } from './page-links.js';
|
|
17
|
+
import { touchesStructuralRows } from './structural-keys.js';
|
|
18
|
+
import { enableTitle, watchSoloModifier } from './solo-click.js';
|
|
19
|
+
import { ENABLE_TITLE, viewerUrl } from '../script-display.js';
|
|
20
|
+
import { ignoreResizeObserverLoopErrors } from './resize-observer-loop.js';
|
|
21
|
+
import { probeUserScriptsApi, USER_SCRIPTS_OFF_HEADING, userScriptsFixLine } from './user-scripts-access.js';
|
|
22
|
+
// The checkbox tooltip's second line under a disabled collection: the
|
|
23
|
+
// box still sets the script's own flag, which has no effect for now.
|
|
24
|
+
const SCRIPT_COLLECTION_OFF_HINT = 'Script will not run because its collection is disabled';
|
|
25
|
+
// Why a disabled script's per-script reloads are greyed.
|
|
26
|
+
const INACTIVE_RELOAD_TITLE = 'This script is disabled, so it isn\'t running on this page';
|
|
27
|
+
// More menu commands than this start collapsed behind a disclosure row.
|
|
28
|
+
const COMMANDS_SHOWN_OPEN = 4;
|
|
29
|
+
const params = new URLSearchParams(location.search);
|
|
30
|
+
const tabParam = params.get('tab');
|
|
31
|
+
// In a real popup, opening a page or running a command should close
|
|
32
|
+
// it, as Chrome's own menus do. In a tab (tests) there is nothing to
|
|
33
|
+
// close, unless `&close=1` asks for the popup's behavior anyway.
|
|
34
|
+
const inPopup = tabParam === null || params.has('close');
|
|
35
|
+
const bannersEl = document.getElementById('banners');
|
|
36
|
+
const scriptsEl = document.getElementById('scripts');
|
|
37
|
+
const pageMenuBtn = document.getElementById('page-menu');
|
|
38
|
+
let tabId = null;
|
|
39
|
+
// The current port to the worker (`connectToWorker`).
|
|
40
|
+
let workerPort = null;
|
|
41
|
+
let view = null;
|
|
42
|
+
// "Allow User Scripts" is off, so nothing injects: probed by the popup
|
|
43
|
+
// itself on each refresh, since the view comes from the worker.
|
|
44
|
+
let userScriptsOff = false;
|
|
45
|
+
// Scripts whose command list the person opened or closed in this popup,
|
|
46
|
+
// overriding the default (closed past COMMANDS_SHOWN_OPEN). The worker
|
|
47
|
+
// keeps them for the tab's lifetime (`view.commandsOpen`); these cover
|
|
48
|
+
// a toggle until the next view brings it back.
|
|
49
|
+
const commandsOpen = new Map();
|
|
50
|
+
// The Open button and the header's home links (icon, product name).
|
|
51
|
+
for (const home of document.querySelectorAll('#open-collections, a.page-home')) {
|
|
52
|
+
onOpenClick(home, namedPageUrl('collections'), () => openNamedTab('collections').then(closeSoon));
|
|
53
|
+
}
|
|
54
|
+
pageMenuBtn.addEventListener('click', () => {
|
|
55
|
+
openMenu(pageMenuBtn, [
|
|
56
|
+
// A plain reload, every script included. The popup stays open
|
|
57
|
+
// through it, as for the selective reloads, and redraws.
|
|
58
|
+
{ label: 'Reload page', run: () => reloadPlain(), hidden: tabId === null },
|
|
59
|
+
{
|
|
60
|
+
label: RELOAD_NO_SCRIPTS_LABEL,
|
|
61
|
+
run: () => reloadTab({ kind: 'none' }),
|
|
62
|
+
// Nothing to keep out on a page no script matches.
|
|
63
|
+
hidden: !view?.perScriptReload
|
|
64
|
+
|| (view.enabled.length === 0 && view.frames.length === 0 && view.disabled.length === 0),
|
|
65
|
+
},
|
|
66
|
+
namedPageItem('SourceMonkey options', 'options'),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
async function start() {
|
|
70
|
+
tabId = tabParam !== null ? Number(tabParam) : await activeTabId();
|
|
71
|
+
if (tabId !== null)
|
|
72
|
+
connectToWorker(tabId);
|
|
73
|
+
// Names, enabled flags and the collection list live in storage. Only
|
|
74
|
+
// those rows: the session area churns with every tab's state and
|
|
75
|
+
// every Log entry, and the worker reports this tab's state itself.
|
|
76
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
77
|
+
if (area === 'local' && touchesStructuralRows(Object.keys(changes)))
|
|
78
|
+
scheduleRefresh();
|
|
79
|
+
});
|
|
80
|
+
await refresh();
|
|
81
|
+
}
|
|
82
|
+
// The port the worker reports this tab's changes over (badge state,
|
|
83
|
+
// menu commands). It drops when the worker goes idle or restarts, so it
|
|
84
|
+
// is reopened, with a refresh for whatever was missed meanwhile.
|
|
85
|
+
function connectToWorker(id) {
|
|
86
|
+
const port = chrome.runtime.connect({ name: POPUP_PORT_PREFIX + id });
|
|
87
|
+
workerPort = port;
|
|
88
|
+
port.onMessage.addListener((message) => {
|
|
89
|
+
const type = message?.type;
|
|
90
|
+
if (type === POPUP_CHANGED_MESSAGE)
|
|
91
|
+
scheduleRefresh();
|
|
92
|
+
else if (type === POPUP_CLOSE_MESSAGE)
|
|
93
|
+
closeSoon();
|
|
94
|
+
});
|
|
95
|
+
port.onDisconnect.addListener(() => {
|
|
96
|
+
// Read, so Chrome doesn't report it as unchecked.
|
|
97
|
+
void chrome.runtime.lastError;
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
connectToWorker(id);
|
|
100
|
+
scheduleRefresh();
|
|
101
|
+
}, 1000);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function activeTabId() {
|
|
105
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
106
|
+
return tab?.id ?? null;
|
|
107
|
+
}
|
|
108
|
+
// Changes come in bursts (a toggle writes storage, then the worker
|
|
109
|
+
// re-badges every tab), so they collapse into one redraw.
|
|
110
|
+
let refreshTimer = null;
|
|
111
|
+
function scheduleRefresh() {
|
|
112
|
+
if (refreshTimer !== null)
|
|
113
|
+
return;
|
|
114
|
+
refreshTimer = setTimeout(() => {
|
|
115
|
+
refreshTimer = null;
|
|
116
|
+
quietly(refresh());
|
|
117
|
+
}, 50);
|
|
118
|
+
}
|
|
119
|
+
// Refreshes can overlap (a toggle's storage write, then the worker's
|
|
120
|
+
// report), and their replies can arrive out of order: only the latest
|
|
121
|
+
// request's reply is drawn.
|
|
122
|
+
let refreshSeq = 0;
|
|
123
|
+
// The last view drawn, serialized, so a refresh that changed nothing
|
|
124
|
+
// doesn't redraw (which would close an open menu).
|
|
125
|
+
let drawnJson = null;
|
|
126
|
+
async function refresh() {
|
|
127
|
+
if (tabId === null) {
|
|
128
|
+
render(null);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const seq = ++refreshSeq;
|
|
132
|
+
const req = { type: POPUP_VIEW_MESSAGE, tabId, keepIds: layoutIds(), keepUrl: layoutUrl ?? undefined };
|
|
133
|
+
let next;
|
|
134
|
+
const probe = probeUserScriptsApi();
|
|
135
|
+
try {
|
|
136
|
+
next = (await chrome.runtime.sendMessage(req));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
next = null;
|
|
140
|
+
}
|
|
141
|
+
const off = !(await probe).usable;
|
|
142
|
+
if (seq !== refreshSeq)
|
|
143
|
+
return;
|
|
144
|
+
recheckBannerAt(next?.bannerRecheckAt ?? null);
|
|
145
|
+
const json = JSON.stringify({ next, off });
|
|
146
|
+
if (json === drawnJson)
|
|
147
|
+
return;
|
|
148
|
+
drawnJson = json;
|
|
149
|
+
view = next;
|
|
150
|
+
userScriptsOff = off;
|
|
151
|
+
render(view);
|
|
152
|
+
}
|
|
153
|
+
// A refresh for when the page stops settling, which nothing reports
|
|
154
|
+
// should the worker's own post-load check not come first.
|
|
155
|
+
let recheckTimer = null;
|
|
156
|
+
function recheckBannerAt(at) {
|
|
157
|
+
if (recheckTimer !== null)
|
|
158
|
+
clearTimeout(recheckTimer);
|
|
159
|
+
recheckTimer = at === null ? null : setTimeout(() => {
|
|
160
|
+
recheckTimer = null;
|
|
161
|
+
scheduleRefresh();
|
|
162
|
+
}, Math.max(0, at - Date.now()) + 100);
|
|
163
|
+
}
|
|
164
|
+
function closeSoon() {
|
|
165
|
+
if (!inPopup)
|
|
166
|
+
return;
|
|
167
|
+
// So the worker doesn't take this close for the second click of a
|
|
168
|
+
// double click on the icon.
|
|
169
|
+
const msg = { type: POPUP_CLOSING_MESSAGE };
|
|
170
|
+
try {
|
|
171
|
+
workerPort?.postMessage(msg);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Disconnected; nothing is waiting on it.
|
|
175
|
+
}
|
|
176
|
+
window.close();
|
|
177
|
+
}
|
|
178
|
+
// Run a click's action, logging rather than dropping a failure (the tab
|
|
179
|
+
// closed meanwhile, say). An unhandled rejection in an extension page
|
|
180
|
+
// lands in chrome://extensions/?errors, where it reads as SourceMonkey
|
|
181
|
+
// being broken.
|
|
182
|
+
function quietly(work) {
|
|
183
|
+
work.catch((err) => console.info('[SourceMonkey] popup action failed:', err));
|
|
184
|
+
}
|
|
185
|
+
// ---------- Rendering ----------
|
|
186
|
+
// Whether script rows are on screen, so a banner coming or going now
|
|
187
|
+
// does so under the pointer, rather than as part of the first draw.
|
|
188
|
+
let listShown = false;
|
|
189
|
+
function render(v) {
|
|
190
|
+
// A redraw while a menu is open would leave it pointing at a button
|
|
191
|
+
// that is gone.
|
|
192
|
+
closeMenu();
|
|
193
|
+
// Emptying the scroll box would scroll it back to the top.
|
|
194
|
+
const scrollTop = scriptsEl.scrollTop;
|
|
195
|
+
const listHeight = scriptsEl.clientHeight;
|
|
196
|
+
const scrolling = scriptsEl.scrollHeight > scriptsEl.clientHeight;
|
|
197
|
+
const bannersHeight = bannersEl.offsetHeight;
|
|
198
|
+
const anchor = listShown ? topAnchor() : null;
|
|
199
|
+
try {
|
|
200
|
+
draw(v);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
scriptsEl.scrollTop = scrollTop;
|
|
204
|
+
const grew = bannersEl.offsetHeight - bannersHeight;
|
|
205
|
+
if (grew !== 0 && listShown)
|
|
206
|
+
keepListInPlace(grew, scrollTop, listHeight, scrolling);
|
|
207
|
+
if (anchor)
|
|
208
|
+
restoreAnchor(anchor);
|
|
209
|
+
listShown = scriptsEl.querySelector('.row') !== null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function topAnchor() {
|
|
213
|
+
const listTop = scriptsEl.getBoundingClientRect().top;
|
|
214
|
+
for (const e of scriptsEl.querySelectorAll('.row, .group-head')) {
|
|
215
|
+
const r = e.getBoundingClientRect();
|
|
216
|
+
if (r.bottom > listTop)
|
|
217
|
+
return { key: anchorKey(e), top: r.top };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function restoreAnchor(anchor) {
|
|
222
|
+
for (const e of scriptsEl.querySelectorAll('.row, .group-head')) {
|
|
223
|
+
if (anchorKey(e) !== anchor.key)
|
|
224
|
+
continue;
|
|
225
|
+
scriptsEl.scrollTop += e.getBoundingClientRect().top - anchor.top;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// A collection can head a group in more than one section; a script is
|
|
230
|
+
// in just one.
|
|
231
|
+
function anchorKey(e) {
|
|
232
|
+
const section = e.closest('section')?.getAttribute('data-section') ?? '';
|
|
233
|
+
const script = e.getAttribute('data-script-id');
|
|
234
|
+
return script ? `script:${script}` : `${section}:${e.getAttribute('data-collection-id') ?? ''}`;
|
|
235
|
+
}
|
|
236
|
+
// A banner appeared above the list while the popup was open (the
|
|
237
|
+
// reload banner, after a toggle). Keep the rows where they were, so the
|
|
238
|
+
// one just clicked stays under the pointer: the banner covers the top
|
|
239
|
+
// of the list's view rather than pushing it down.
|
|
240
|
+
// - Already scrolling, the popup is at its full height and the list
|
|
241
|
+
// just got shorter: scroll it on by the banner's height.
|
|
242
|
+
// - Not scrolling, pin the list to its old height less the banner's,
|
|
243
|
+
// scrolled down by that much. Scrolling back up grows the popup
|
|
244
|
+
// instead (`growPinnedList`), and the pin goes once the top is back.
|
|
245
|
+
// - Not when the banner would hide more than it leaves: then the popup
|
|
246
|
+
// just grows.
|
|
247
|
+
// The banner going (ticking the box back, say) undoes this: the list
|
|
248
|
+
// scrolls back by as much, and a pinned one grows into the space.
|
|
249
|
+
// The list has no padding or border, so its heights are its content's.
|
|
250
|
+
function keepListInPlace(grew, scrollTop, listHeight, scrolling) {
|
|
251
|
+
const pinned = scriptsEl.style.maxHeight !== '';
|
|
252
|
+
if (grew < 0 && pinned) {
|
|
253
|
+
scriptsEl.style.maxHeight = `${listHeight - grew}px`;
|
|
254
|
+
}
|
|
255
|
+
else if (grew > 0 && !scrolling) {
|
|
256
|
+
if (listHeight - grew < grew)
|
|
257
|
+
return;
|
|
258
|
+
scriptsEl.style.maxHeight = `${listHeight - grew}px`;
|
|
259
|
+
}
|
|
260
|
+
else if (!scrolling) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
scriptsEl.scrollTop = scrollTop + grew;
|
|
264
|
+
growPinnedList();
|
|
265
|
+
}
|
|
266
|
+
// Only grows: rows added while pinned (a command list opened, say) are
|
|
267
|
+
// scrolled to as usual, without the popup shrinking.
|
|
268
|
+
function growPinnedList() {
|
|
269
|
+
if (!scriptsEl.style.maxHeight)
|
|
270
|
+
return;
|
|
271
|
+
if (scriptsEl.scrollTop <= 0)
|
|
272
|
+
unpinList();
|
|
273
|
+
else {
|
|
274
|
+
const height = Math.max(parseFloat(scriptsEl.style.maxHeight), scriptsEl.scrollHeight - scriptsEl.scrollTop);
|
|
275
|
+
scriptsEl.style.maxHeight = `${height}px`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function unpinList() {
|
|
279
|
+
scriptsEl.style.maxHeight = '';
|
|
280
|
+
}
|
|
281
|
+
scriptsEl.addEventListener('scroll', growPinnedList);
|
|
282
|
+
// The popup is always wider by a scrollbar (see popup.html). Without
|
|
283
|
+
// one, the rows pad that strip (`--gutter`); with one, the scrollbar
|
|
284
|
+
// fills it. Either way the rows keep their width and don't rewrap. The
|
|
285
|
+
// list's content box changes width as a scrollbar comes or goes, which
|
|
286
|
+
// the observer sees before the frame is painted.
|
|
287
|
+
const scrollbarWidth = measureScrollbar();
|
|
288
|
+
document.body.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
|
|
289
|
+
// Set before the first frame too, so the first frame needs no fix-up.
|
|
290
|
+
// Later, when a scrollbar comes or goes, the frame lays out once with
|
|
291
|
+
// both scrollbar and gutter; a row can rewrap, and the observer's fix
|
|
292
|
+
// resizes the list again in the same frame. Chrome reports that as a
|
|
293
|
+
// ResizeObserver loop error, which is harmless and kept out of the
|
|
294
|
+
// Errors panel.
|
|
295
|
+
scriptsEl.style.setProperty('--gutter', `${scrollbarWidth}px`);
|
|
296
|
+
ignoreResizeObserverLoopErrors();
|
|
297
|
+
new ResizeObserver(() => {
|
|
298
|
+
const scrolling = scriptsEl.offsetWidth > scriptsEl.clientWidth;
|
|
299
|
+
scriptsEl.style.setProperty('--gutter', scrolling ? '0px' : `${scrollbarWidth}px`);
|
|
300
|
+
}).observe(scriptsEl);
|
|
301
|
+
// At a fractional scale (`devicePixelRatio` 1.359375, say), a popup
|
|
302
|
+
// of whole CSS px can end partway through a screen pixel: 223px is
|
|
303
|
+
// 303.14 screen rows. Chrome shows only the whole rows (303, which
|
|
304
|
+
// `visualViewport` reports), but places the fixed border overlay as if
|
|
305
|
+
// the page went on to the next whole row (304). Its bottom line then
|
|
306
|
+
// falls in the row that isn't shown. So the overlay ends short by the
|
|
307
|
+
// rows not shown (none at a whole-number scale). Chrome resizes the
|
|
308
|
+
// popup as its content changes, which fires `resize`.
|
|
309
|
+
// - The small epsilon keeps float error from rounding up a product
|
|
310
|
+
// that should be whole (200 * 1.1 is 220.00000000000003).
|
|
311
|
+
// - At most one screen pixel: anything more (a page scrollbar or pinch
|
|
312
|
+
// zoom in a tab, where `visualViewport` is smaller for other
|
|
313
|
+
// reasons) isn't this rounding.
|
|
314
|
+
function trimEdges() {
|
|
315
|
+
const dpr = window.devicePixelRatio;
|
|
316
|
+
const trim = (laid, shown) => {
|
|
317
|
+
const rows = Math.ceil(laid * dpr - 1e-6) - Math.round(shown * dpr);
|
|
318
|
+
return `${Math.min(Math.max(rows, 0), 1) / dpr}px`;
|
|
319
|
+
};
|
|
320
|
+
document.body.style.setProperty('--edge-trim-x', trim(window.innerWidth, visualViewport.width));
|
|
321
|
+
document.body.style.setProperty('--edge-trim-y', trim(window.innerHeight, visualViewport.height));
|
|
322
|
+
}
|
|
323
|
+
trimEdges();
|
|
324
|
+
window.addEventListener('resize', trimEdges);
|
|
325
|
+
function measureScrollbar() {
|
|
326
|
+
const probe = el('div', { style: 'position: absolute; visibility: hidden; overflow: scroll; width: 100px' });
|
|
327
|
+
document.body.append(probe);
|
|
328
|
+
const width = probe.offsetWidth - probe.clientWidth;
|
|
329
|
+
probe.remove();
|
|
330
|
+
return width;
|
|
331
|
+
}
|
|
332
|
+
function draw(v) {
|
|
333
|
+
bannersEl.replaceChildren();
|
|
334
|
+
scriptsEl.replaceChildren();
|
|
335
|
+
document.body.dataset.ready = 'true';
|
|
336
|
+
// On every page: it's why no page runs scripts.
|
|
337
|
+
if (userScriptsOff) {
|
|
338
|
+
bannersEl.append(el('div', { class: 'banner-row user-scripts-off' }, el('span', {}, el('strong', {}, USER_SCRIPTS_OFF_HEADING), userScriptsFixLine('span'))));
|
|
339
|
+
}
|
|
340
|
+
if (!v || !v.injectable) {
|
|
341
|
+
scriptsEl.append(el('div', { class: 'empty-page' }, POPUP_NOT_INJECTABLE));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (v.smDev)
|
|
345
|
+
bannersEl.append(el('div', { class: 'banner-row sm-dev' }, el('span', {}, SM_DEV_CONTROL_LABEL)));
|
|
346
|
+
// Not while user scripts are off: a reload wouldn't run them. Once
|
|
347
|
+
// they're on again, a page that loaded meanwhile asks for a reload
|
|
348
|
+
// (UNTRACKED).
|
|
349
|
+
if (v.banner && !userScriptsOff) {
|
|
350
|
+
const reload = el('button', { type: 'button' }, 'Reload');
|
|
351
|
+
reload.addEventListener('click', () => quietly(reloadPlain()));
|
|
352
|
+
bannersEl.append(el('div', { class: 'banner-row reload' }, el('span', {}, v.banner), reload));
|
|
353
|
+
}
|
|
354
|
+
const { enabled, frames, manual, disabled } = arrange(v);
|
|
355
|
+
if (enabled.length === 0 && frames.length === 0 && manual.length === 0 && disabled.length === 0) {
|
|
356
|
+
scriptsEl.append(el('div', { class: 'empty-page' }, POPUP_NO_SCRIPTS));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
renderSection('Enabled', enabled, 'No enabled scripts match this page.');
|
|
360
|
+
// Only when there are some: scripts that run in the page's iframes
|
|
361
|
+
// without matching the page itself (docs/frames.md).
|
|
362
|
+
if (frames.length > 0)
|
|
363
|
+
renderSection('Active in frames', frames, '');
|
|
364
|
+
// Only when there are some: most pages have no manual scripts.
|
|
365
|
+
if (manual.length > 0)
|
|
366
|
+
renderSection('Manual', manual, '');
|
|
367
|
+
renderSection('Disabled', disabled, 'None');
|
|
368
|
+
}
|
|
369
|
+
const SECTION_KEYS = ['enabled', 'frames', 'manual', 'disabled'];
|
|
370
|
+
let layout = null;
|
|
371
|
+
// The URL the layout was placed for. On another URL (the page
|
|
372
|
+
// navigated) it's another page's layout, and is placed afresh.
|
|
373
|
+
let layoutUrl = null;
|
|
374
|
+
function layoutIds() {
|
|
375
|
+
if (!layout)
|
|
376
|
+
return [];
|
|
377
|
+
return SECTION_KEYS.flatMap((k) => layout[k]).flatMap((g) => g.ids);
|
|
378
|
+
}
|
|
379
|
+
function arrange(v) {
|
|
380
|
+
// Where the view would put each row now.
|
|
381
|
+
const fresh = new Map();
|
|
382
|
+
for (const section of SECTION_KEYS) {
|
|
383
|
+
for (const g of v[section])
|
|
384
|
+
for (const row of g.rows)
|
|
385
|
+
fresh.set(row.id, { row, collectionId: g.collection.id, section });
|
|
386
|
+
}
|
|
387
|
+
if (layoutUrl !== v.url) {
|
|
388
|
+
layout = null;
|
|
389
|
+
layoutUrl = v.url;
|
|
390
|
+
}
|
|
391
|
+
if (!layout) {
|
|
392
|
+
const place = (groups) => groups.map((g) => ({ collectionId: g.collection.id, ids: g.rows.map((r) => r.id) }));
|
|
393
|
+
layout = { enabled: place(v.enabled), frames: place(v.frames), manual: place(v.manual), disabled: place(v.disabled) };
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// A row whose collection changed (the script moved) leaves its old
|
|
397
|
+
// group; then anything not placed yet goes at the end of its group,
|
|
398
|
+
// or in a new group at the end of its section.
|
|
399
|
+
const placed = new Set();
|
|
400
|
+
for (const section of SECTION_KEYS) {
|
|
401
|
+
for (const g of layout[section]) {
|
|
402
|
+
g.ids = g.ids.filter((id) => {
|
|
403
|
+
const now = fresh.get(id);
|
|
404
|
+
return !now || now.collectionId === g.collectionId;
|
|
405
|
+
});
|
|
406
|
+
for (const id of g.ids)
|
|
407
|
+
placed.add(id);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const [id, info] of fresh) {
|
|
411
|
+
if (placed.has(id))
|
|
412
|
+
continue;
|
|
413
|
+
const groups = layout[info.section];
|
|
414
|
+
let group = groups.find((g) => g.collectionId === info.collectionId);
|
|
415
|
+
if (!group) {
|
|
416
|
+
group = { collectionId: info.collectionId, ids: [] };
|
|
417
|
+
groups.push(group);
|
|
418
|
+
}
|
|
419
|
+
group.ids.push(id);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const collections = new Map(v.collections.map((c) => [c.id, c]));
|
|
423
|
+
const build = (groups) => groups
|
|
424
|
+
.map((g) => ({
|
|
425
|
+
collection: collections.get(g.collectionId),
|
|
426
|
+
// A row the view no longer has (its script was deleted) drops out.
|
|
427
|
+
rows: g.ids.map((id) => fresh.get(id)?.row).filter((r) => r !== undefined),
|
|
428
|
+
}))
|
|
429
|
+
.filter((g) => g.collection !== undefined && g.rows.length > 0);
|
|
430
|
+
return {
|
|
431
|
+
enabled: build(layout.enabled), frames: build(layout.frames), manual: build(layout.manual), disabled: build(layout.disabled),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function renderSection(title, groups, emptyText) {
|
|
435
|
+
const section = el('section', { class: 'script-section', 'data-section': title.toLowerCase().replace(/ /g, '-') });
|
|
436
|
+
section.append(el('div', { class: 'section-head' }, title));
|
|
437
|
+
if (groups.length === 0)
|
|
438
|
+
section.append(el('div', { class: 'empty-note' }, emptyText));
|
|
439
|
+
for (const group of groups) {
|
|
440
|
+
section.append(groupHead(group));
|
|
441
|
+
for (const row of group.rows)
|
|
442
|
+
section.append(scriptRow(row, group));
|
|
443
|
+
}
|
|
444
|
+
scriptsEl.append(section);
|
|
445
|
+
}
|
|
446
|
+
// A pill whose click reloads the page, as the banner's Reload does.
|
|
447
|
+
function reloadPill(row, status) {
|
|
448
|
+
const pill = el('button', {
|
|
449
|
+
type: 'button',
|
|
450
|
+
class: `pill ${PILL_CLASSES[status]}`,
|
|
451
|
+
title: row.tooltip ?? '',
|
|
452
|
+
'data-status': status,
|
|
453
|
+
}, POPUP_STATUS_LABELS[status]);
|
|
454
|
+
pill.addEventListener('click', () => quietly(reloadPlain()));
|
|
455
|
+
return pill;
|
|
456
|
+
}
|
|
457
|
+
function groupHead(group) {
|
|
458
|
+
const c = group.collection;
|
|
459
|
+
// Scripts removed since the page loaded: a plain heading, with no
|
|
460
|
+
// collection to open or toggle.
|
|
461
|
+
if (c.id === REMOVED_GROUP_ID) {
|
|
462
|
+
return el('div', { class: 'group-head', 'data-collection-id': c.id }, el('span', { class: 'collection-name' }, c.name));
|
|
463
|
+
}
|
|
464
|
+
const hash = collectionAnchorId(c.id);
|
|
465
|
+
const name = el('a', {
|
|
466
|
+
class: 'collection-name', href: namedPageUrl('collections', hash), target: '_blank',
|
|
467
|
+
title: 'Show this collection on the collections page',
|
|
468
|
+
}, c.name);
|
|
469
|
+
onOpenClick(name, namedPageUrl('collections', hash), () => openNamedTab('collections', { hash }).then(closeSoon));
|
|
470
|
+
const off = !c.enabled;
|
|
471
|
+
const menuBtn = moreMenuButton('Collection actions');
|
|
472
|
+
menuBtn.addEventListener('click', () => openMenu(menuBtn, [
|
|
473
|
+
namedPageItem('Collection details', 'collections', hash),
|
|
474
|
+
{
|
|
475
|
+
label: off ? 'Enable collection' : 'Disable collection',
|
|
476
|
+
run: () => setEnabled('collection', c.id, off),
|
|
477
|
+
},
|
|
478
|
+
namedPageItem('Collection options', 'collections', EDIT_COLLECTION_HASH_PREFIX + c.id),
|
|
479
|
+
]));
|
|
480
|
+
const head = el('div', { class: 'group-head' + (off ? ' off' : ''), 'data-collection-id': c.id }, name, makeCollectionTypeBubble(c.path));
|
|
481
|
+
if (off)
|
|
482
|
+
head.append(el('span', { class: 'collection-off', title: COLLECTION_OFF_TOOLTIP }, COLLECTION_OFF_LABEL));
|
|
483
|
+
head.append(el('span', { class: 'spacer' }), menuBtn);
|
|
484
|
+
return head;
|
|
485
|
+
}
|
|
486
|
+
const PILL_CLASSES = {
|
|
487
|
+
OK: 'ok', ERROR: 'error', REJECTED: 'rejected', RELOAD: 'reload', MISSING: 'missing',
|
|
488
|
+
UNEXPECTED: 'unexpected', NOT_LOADED: 'not-loaded', UNTRACKED: 'untracked', PENDING: 'pending', STILL_RUNNING: 'still-running',
|
|
489
|
+
WARNING: 'warning', INVALID: 'invalid', NOT_RUN: 'not-run',
|
|
490
|
+
};
|
|
491
|
+
function scriptRow(row, group) {
|
|
492
|
+
if (group.collection.id === REMOVED_GROUP_ID)
|
|
493
|
+
return removedRow(row);
|
|
494
|
+
const boxTitle = group.collection.enabled
|
|
495
|
+
? enableTitle(row.otherCopies)
|
|
496
|
+
: `${ENABLE_TITLE}\n${SCRIPT_COLLECTION_OFF_HINT}`;
|
|
497
|
+
const box = el('input', { type: 'checkbox', title: boxTitle });
|
|
498
|
+
box.checked = row.checked;
|
|
499
|
+
// Exclusive, as everywhere: turning it on turns the script's other
|
|
500
|
+
// active copies off, unless Ctrl/Cmd-clicked.
|
|
501
|
+
const takeSolo = watchSoloModifier(box);
|
|
502
|
+
box.addEventListener('change', () => quietly(setEnabled('script', row.id, box.checked, takeSolo())));
|
|
503
|
+
const name = el('a', { class: 'script-name', href: viewerUrl(row.id), target: '_blank', title: 'Open the script page' }, row.name);
|
|
504
|
+
onOpenClick(name, viewerUrl(row.id));
|
|
505
|
+
const main = el('div', { class: 'row-main' }, box, name);
|
|
506
|
+
if (row.status === 'PENDING' || row.status === 'UNTRACKED') {
|
|
507
|
+
// Reload to run, or Reload: a click does just that.
|
|
508
|
+
main.append(reloadPill(row, row.status));
|
|
509
|
+
}
|
|
510
|
+
else if (row.status) {
|
|
511
|
+
const pill = el('a', {
|
|
512
|
+
class: `pill ${PILL_CLASSES[row.status]}`,
|
|
513
|
+
href: viewerUrl(row.id, 'log'),
|
|
514
|
+
target: '_blank',
|
|
515
|
+
title: row.tooltip ?? '',
|
|
516
|
+
'data-status': row.status,
|
|
517
|
+
}, POPUP_STATUS_LABELS[row.status]);
|
|
518
|
+
onOpenClick(pill, viewerUrl(row.id, 'log'));
|
|
519
|
+
main.append(pill);
|
|
520
|
+
}
|
|
521
|
+
const menuBtn = moreMenuButton('Script actions');
|
|
522
|
+
// Not for a manual script: no reload runs it, so running it alone or
|
|
523
|
+
// keeping it out is nothing.
|
|
524
|
+
const perScriptReload = view?.perScriptReload === true && !row.manual;
|
|
525
|
+
menuBtn.addEventListener('click', () => openMenu(menuBtn, [
|
|
526
|
+
pageItem('Script details', viewerUrl(row.id)),
|
|
527
|
+
pageItem('Script log', viewerUrl(row.id, 'log')),
|
|
528
|
+
{ separator: true, hidden: !perScriptReload },
|
|
529
|
+
// Greyed for a script that won't run (its box or its collection is
|
|
530
|
+
// off): running it alone or keeping it out would do nothing.
|
|
531
|
+
{
|
|
532
|
+
label: 'Reload page with only this script', run: () => reloadTab({ kind: 'only', scriptId: row.id }),
|
|
533
|
+
hidden: !perScriptReload, disabled: !row.active, title: row.active ? undefined : INACTIVE_RELOAD_TITLE,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
label: 'Reload page without this script', run: () => reloadTab({ kind: 'without', scriptId: row.id }),
|
|
537
|
+
hidden: !perScriptReload, disabled: !row.active, title: row.active ? undefined : INACTIVE_RELOAD_TITLE,
|
|
538
|
+
},
|
|
539
|
+
]));
|
|
540
|
+
main.append(menuBtn);
|
|
541
|
+
const cls = 'row' + (row.active ? '' : ' off') + (row.checked && !group.collection.enabled ? ' muted' : '');
|
|
542
|
+
const out = el('div', { class: cls, 'data-script-id': row.id }, main);
|
|
543
|
+
if (row.versionLine)
|
|
544
|
+
out.append(el('div', { class: 'version-line' }, row.versionLine));
|
|
545
|
+
if (row.commands.length > 0 || row.runnable)
|
|
546
|
+
out.append(commandList(row));
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
// A script removed since the page loaded that still runs in it: its
|
|
550
|
+
// name, status and menu commands, with nothing to toggle or open.
|
|
551
|
+
function removedRow(row) {
|
|
552
|
+
const main = el('div', { class: 'row-main' }, el('span', { class: 'script-name' }, row.name));
|
|
553
|
+
if (row.status) {
|
|
554
|
+
main.append(el('span', {
|
|
555
|
+
class: `pill ${PILL_CLASSES[row.status]}`, title: row.tooltip ?? '', 'data-status': row.status,
|
|
556
|
+
}, POPUP_STATUS_LABELS[row.status]));
|
|
557
|
+
}
|
|
558
|
+
const out = el('div', { class: 'row off', 'data-script-id': row.id }, main);
|
|
559
|
+
if (row.commands.length > 0)
|
|
560
|
+
out.append(commandList(row));
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
function commandList(row) {
|
|
564
|
+
const list = el('div', { class: 'commands' });
|
|
565
|
+
// A manual script's own item comes first, and never folds away with
|
|
566
|
+
// the script's commands.
|
|
567
|
+
if (row.runnable) {
|
|
568
|
+
const run = el('button', { type: 'button', class: 'command run-script' }, RUN_SCRIPT_LABEL);
|
|
569
|
+
run.addEventListener('click', () => quietly(runScript(row.id)));
|
|
570
|
+
list.append(run);
|
|
571
|
+
}
|
|
572
|
+
if (row.commands.length === 0)
|
|
573
|
+
return list;
|
|
574
|
+
const long = row.commands.length > COMMANDS_SHOWN_OPEN;
|
|
575
|
+
const open = commandsOpen.get(row.id) ?? view?.commandsOpen?.[row.id] ?? !long;
|
|
576
|
+
if (long) {
|
|
577
|
+
const toggle = el('button', { type: 'button', class: 'disclose', 'aria-expanded': String(open) }, open ? '▾ Hide commands' : `▸ ${row.commands.length} commands`);
|
|
578
|
+
toggle.addEventListener('click', () => {
|
|
579
|
+
commandsOpen.set(row.id, !open);
|
|
580
|
+
render(view);
|
|
581
|
+
if (tabId !== null) {
|
|
582
|
+
const req = {
|
|
583
|
+
type: POPUP_SET_COMMANDS_OPEN_MESSAGE, tabId, scriptId: row.id, open: !open,
|
|
584
|
+
};
|
|
585
|
+
quietly(chrome.runtime.sendMessage(req));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
list.append(toggle);
|
|
589
|
+
}
|
|
590
|
+
if (!open)
|
|
591
|
+
return list;
|
|
592
|
+
for (const cmd of row.commands) {
|
|
593
|
+
const btn = el('button', { type: 'button', class: 'command' }, cmd.name);
|
|
594
|
+
btn.addEventListener('click', () => quietly(runCommand(row.id, cmd.commandId, cmd.documentId)));
|
|
595
|
+
list.append(btn);
|
|
596
|
+
}
|
|
597
|
+
return list;
|
|
598
|
+
}
|
|
599
|
+
function openHow(ev) {
|
|
600
|
+
if (ev.altKey)
|
|
601
|
+
return null;
|
|
602
|
+
if (ev.button === 1 || (ev.button === 0 && (ev.ctrlKey || ev.metaKey))) {
|
|
603
|
+
return ev.shiftKey ? 'tab' : 'background';
|
|
604
|
+
}
|
|
605
|
+
if (ev.button !== 0)
|
|
606
|
+
return null;
|
|
607
|
+
return ev.shiftKey ? 'window' : 'plain';
|
|
608
|
+
}
|
|
609
|
+
// Wire `target` to open `url` by the click's gesture. `openPlain`
|
|
610
|
+
// defaults to a plain new tab. Each closes the popup only once the page
|
|
611
|
+
// is open: closing it ends this page, and an open still in flight with
|
|
612
|
+
// it. The middle button fires `auxclick`, not `click`.
|
|
613
|
+
function onOpenClick(target, url, openPlain = () => openExtensionTab(url).then(closeSoon), onOpened) {
|
|
614
|
+
const handle = (ev) => {
|
|
615
|
+
const how = openHow(ev);
|
|
616
|
+
if (!how)
|
|
617
|
+
return;
|
|
618
|
+
ev.preventDefault();
|
|
619
|
+
onOpened?.(how);
|
|
620
|
+
if (how === 'plain')
|
|
621
|
+
quietly(openPlain());
|
|
622
|
+
else if (how === 'background')
|
|
623
|
+
quietly(openBackgroundTab(url));
|
|
624
|
+
else if (how === 'tab')
|
|
625
|
+
quietly(openExtensionTab(url).then(closeSoon));
|
|
626
|
+
else
|
|
627
|
+
quietly(openExtensionWindow(url).then(closeSoon));
|
|
628
|
+
};
|
|
629
|
+
target.addEventListener('click', handle);
|
|
630
|
+
target.addEventListener('auxclick', handle);
|
|
631
|
+
}
|
|
632
|
+
// ---------- Actions ----------
|
|
633
|
+
async function reloadPlain() {
|
|
634
|
+
if (tabId !== null)
|
|
635
|
+
await chrome.tabs.reload(tabId);
|
|
636
|
+
}
|
|
637
|
+
async function reloadTab(mode) {
|
|
638
|
+
if (tabId === null)
|
|
639
|
+
return;
|
|
640
|
+
const req = { type: POPUP_RELOAD_MESSAGE, tabId, mode };
|
|
641
|
+
await chrome.runtime.sendMessage(req);
|
|
642
|
+
}
|
|
643
|
+
async function runCommand(scriptId, commandId, documentId) {
|
|
644
|
+
if (tabId === null)
|
|
645
|
+
return;
|
|
646
|
+
const req = {
|
|
647
|
+
type: POPUP_COMMAND_MESSAGE, tabId, scriptId, commandId, ...(documentId ? { documentId } : {}),
|
|
648
|
+
};
|
|
649
|
+
await chrome.runtime.sendMessage(req);
|
|
650
|
+
closeSoon();
|
|
651
|
+
}
|
|
652
|
+
// Run a manual script. The popup stays open, so the row's status and
|
|
653
|
+
// any commands the run registers show up as they arrive.
|
|
654
|
+
async function runScript(scriptId) {
|
|
655
|
+
if (tabId === null)
|
|
656
|
+
return;
|
|
657
|
+
const req = { type: POPUP_RUN_SCRIPT_MESSAGE, tabId, scriptId };
|
|
658
|
+
await chrome.runtime.sendMessage(req);
|
|
659
|
+
}
|
|
660
|
+
// Ticks a script's or collection's box. The worker writes the row, and
|
|
661
|
+
// the storage change redraws the popup. When the write fails nothing
|
|
662
|
+
// changes, so the view would match the one drawn and be skipped: forget
|
|
663
|
+
// it, so the redraw puts the box back as stored.
|
|
664
|
+
// `solo`: Ctrl/Cmd-clicked, so other copies are left alone
|
|
665
|
+
// (docs/script-enablement.md).
|
|
666
|
+
async function setEnabled(target, id, enabled, solo = false) {
|
|
667
|
+
const req = { type: POPUP_SET_ENABLED_MESSAGE, target, id, enabled, solo };
|
|
668
|
+
const reply = (await chrome.runtime.sendMessage(req));
|
|
669
|
+
if (reply?.ok !== true) {
|
|
670
|
+
drawnJson = null;
|
|
671
|
+
await refresh();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Menu items that open a page, by any of `onOpenClick`'s gestures.
|
|
675
|
+
function pageItem(label, url) {
|
|
676
|
+
return { label, run: () => openExtensionTab(url).then(closeSoon), url };
|
|
677
|
+
}
|
|
678
|
+
function namedPageItem(label, page, hash) {
|
|
679
|
+
return { label, run: () => openNamedTab(page, { hash }).then(closeSoon), url: namedPageUrl(page, hash) };
|
|
680
|
+
}
|
|
681
|
+
function openMenu(btn, items) {
|
|
682
|
+
openSharedMenu(btn, items.map(({ url, run, ...rest }) => {
|
|
683
|
+
if (url && run) {
|
|
684
|
+
return { ...rest, wire: (b, close) => onOpenClick(b, url, run, (how) => { if (how !== 'background')
|
|
685
|
+
close(); }) };
|
|
686
|
+
}
|
|
687
|
+
return { ...rest, ...(run ? { run: () => quietly(run()) } : {}) };
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
function el(tag, attrs = {}, ...kids) {
|
|
691
|
+
const e = document.createElement(tag);
|
|
692
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
693
|
+
if (k === 'class')
|
|
694
|
+
e.className = v;
|
|
695
|
+
else
|
|
696
|
+
e.setAttribute(k, v);
|
|
697
|
+
}
|
|
698
|
+
e.append(...kids);
|
|
699
|
+
return e;
|
|
700
|
+
}
|
|
701
|
+
// Last, so every module-level binding above is initialized before the
|
|
702
|
+
// first render reads it.
|
|
703
|
+
quietly(start());
|