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,619 @@
|
|
|
1
|
+
// The create / edit collection dialog, shared by the collections page
|
|
2
|
+
// (Add field, Edit button) and the install page ("New collection…" in
|
|
3
|
+
// the Install-into dropdown). One `<dialog>` per page, built on first
|
|
4
|
+
// use; `openCollectionDialog` resolves when it closes.
|
|
5
|
+
//
|
|
6
|
+
// The dialog owns the whole decision: it validates live as the user
|
|
7
|
+
// types (name required / in use, what the location turns out to be),
|
|
8
|
+
// probes a location before creating from it, and shows every failure in
|
|
9
|
+
// its own error slot — so a location that can't be read never lands as
|
|
10
|
+
// a collection stuck in a scan error, and the caller only ever hears
|
|
11
|
+
// about a collection that exists.
|
|
12
|
+
//
|
|
13
|
+
// The type is never asked for, only reported: a Type row shows the
|
|
14
|
+
// collection bubble the form would produce — `scripts` with no
|
|
15
|
+
// location, `<scheme>-dir` / `<scheme>-manifest` for a location we
|
|
16
|
+
// could classify, and `invalid` for anything else.
|
|
17
|
+
//
|
|
18
|
+
// See docs/storage.md#the-collection-dialog.
|
|
19
|
+
import { COLLECTION_ALREADY_REGISTERED, COLLECTION_GONE, COLLECTION_NAME_REQUIRED, collectionNameInUseMessage, collectionUsesLocalFiles, describeError, getCollections, probeCollectionLocation, } from './script-store.js';
|
|
20
|
+
import { createCollection, setCollectionOptions } from './worker-client.js';
|
|
21
|
+
import { canonicalCollectionInput, classifyLocation, collectionKind, SINGLE_SCRIPT_NOT_A_COLLECTION, } from './directories.js';
|
|
22
|
+
import { collectionBubbleKey, inferCollectionType, inferDisplayName, mayUseReservedName, RESERVED_COLLECTION_NAME, SCRIPTS_BUBBLE_KEY, collectionNameInUse, } from './collection-display.js';
|
|
23
|
+
import { MANIFEST_FILENAME } from '../collection-scan.js';
|
|
24
|
+
import { collectionSupportsLocalFiles } from '../local-file-overrides.js';
|
|
25
|
+
import { dlog } from '../debug-log.js';
|
|
26
|
+
import { previewUrl } from '../script-display.js';
|
|
27
|
+
import { openExtensionTab } from './page-links.js';
|
|
28
|
+
import { setGlyphButton, setGlyphButtonLabel } from './glyph.js';
|
|
29
|
+
import { makeCopyButton } from './copy-button.js';
|
|
30
|
+
import { isFileUrl, isHttpUrl, isManifestCollection, pathToFileUrl } from '../file-locations.js';
|
|
31
|
+
const CREATE_WORDING = {
|
|
32
|
+
collectionOnly: { title: 'New collection', ok: 'Create' },
|
|
33
|
+
install: { title: 'Create new collection and install script', ok: 'Create and install' },
|
|
34
|
+
move: { title: 'Create new collection and move script', ok: 'Create and move' },
|
|
35
|
+
};
|
|
36
|
+
// The hint under the local-overrides option, by why it does or doesn't
|
|
37
|
+
// apply. The option is shown for every collection so the dialog's
|
|
38
|
+
// shape doesn't shift with the type; where it can't act, the hint says
|
|
39
|
+
// why instead of leaving a greyed box to wonder about.
|
|
40
|
+
const LOCAL_FILES_HINT = 'Load local versions of @require and @resource files found in the ' +
|
|
41
|
+
'script or collection directories, or in the global search path';
|
|
42
|
+
function localFilesUnavailableHint(location) {
|
|
43
|
+
const where = inferCollectionType(location) === 'github' ? 'from GitHub' : 'over http';
|
|
44
|
+
return `Not applicable for scripts loaded ${where}`;
|
|
45
|
+
}
|
|
46
|
+
// Shown once when a location reads fine but holds no scripts. A
|
|
47
|
+
// warning, not a refusal: a directory can fill up later, so the next
|
|
48
|
+
// Create goes through.
|
|
49
|
+
const EMPTY_LOCATION_WARNING = 'No .user.js files found. Click Create to add this collection anyway.';
|
|
50
|
+
// Bubble keys that are states of the form rather than collection kinds.
|
|
51
|
+
// Both get their own colour in styles.css.
|
|
52
|
+
const INVALID_BUBBLE_KEY = 'invalid';
|
|
53
|
+
const CHECKING_BUBBLE_KEY = 'checking';
|
|
54
|
+
// What each Type bubble means, as its tooltip. The radios this row
|
|
55
|
+
// replaced carried a one-line gloss each; without something here a
|
|
56
|
+
// first-time reader gets only an uppercase token.
|
|
57
|
+
const TYPE_BUBBLE_TITLES = {
|
|
58
|
+
[SCRIPTS_BUBBLE_KEY]: 'Scripts installed one at a time, from anywhere',
|
|
59
|
+
[INVALID_BUBBLE_KEY]: 'Invalid collection specification',
|
|
60
|
+
[CHECKING_BUBBLE_KEY]: 'Reading this location…',
|
|
61
|
+
};
|
|
62
|
+
function typeBubbleTitle(bubbleKey) {
|
|
63
|
+
const known = TYPE_BUBBLE_TITLES[bubbleKey];
|
|
64
|
+
if (known)
|
|
65
|
+
return known;
|
|
66
|
+
if (bubbleKey.endsWith('-manifest'))
|
|
67
|
+
return `Every script listed in this ${MANIFEST_FILENAME}`;
|
|
68
|
+
return bubbleKey.startsWith('github')
|
|
69
|
+
? 'Every .user.js in this GitHub folder'
|
|
70
|
+
: 'Every .user.js under this directory';
|
|
71
|
+
}
|
|
72
|
+
// What the Type row says beside `INVALID` on a dialog nothing has been
|
|
73
|
+
// typed into yet — the one state where the error slot stays empty,
|
|
74
|
+
// because nothing is wrong yet, only unfinished. Naming only what this
|
|
75
|
+
// dialog can actually take: a scripts-only create has no location
|
|
76
|
+
// field to point at.
|
|
77
|
+
function enterSomethingHint(showLocation) {
|
|
78
|
+
return showLocation ? 'Enter a name or path' : 'Enter a name';
|
|
79
|
+
}
|
|
80
|
+
// How long the location field sits still before its verdict is
|
|
81
|
+
// fetched. A local classification reads the disk, so it is not run per
|
|
82
|
+
// keystroke.
|
|
83
|
+
const LOCATION_PROBE_DELAY_MS = 250;
|
|
84
|
+
let elements = null;
|
|
85
|
+
// One dialog at a time, claimed SYNCHRONOUSLY. `dialog.open` can't be
|
|
86
|
+
// the guard: `runDialog` reads the collection list before it calls
|
|
87
|
+
// `showModal`, so during that await a second call would see a closed
|
|
88
|
+
// dialog, pass, and then overwrite every handler the first run
|
|
89
|
+
// installed — leaving the first caller's promise unsettled forever and
|
|
90
|
+
// the second's rejected by `showModal` on an already-open dialog.
|
|
91
|
+
// Reachable by double-clicking Edit.
|
|
92
|
+
let dialogInFlight = false;
|
|
93
|
+
function el(tag, className, text) {
|
|
94
|
+
const node = document.createElement(tag);
|
|
95
|
+
if (className)
|
|
96
|
+
node.className = className;
|
|
97
|
+
if (text !== undefined)
|
|
98
|
+
node.textContent = text;
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
function buildDialog() {
|
|
102
|
+
const dialog = el('dialog', 'sm-dialog collection-dialog');
|
|
103
|
+
const title = el('h2', 'dlg-title');
|
|
104
|
+
dialog.appendChild(title);
|
|
105
|
+
const form = el('form', 'dlg-body');
|
|
106
|
+
form.noValidate = true;
|
|
107
|
+
dialog.appendChild(form);
|
|
108
|
+
const nameLabel = el('label', 'dlg-label', 'Name');
|
|
109
|
+
nameLabel.htmlFor = 'collection-dialog-name';
|
|
110
|
+
const name = el('input');
|
|
111
|
+
name.type = 'text';
|
|
112
|
+
name.id = 'collection-dialog-name';
|
|
113
|
+
name.autocomplete = 'off';
|
|
114
|
+
form.append(nameLabel, name);
|
|
115
|
+
const locationLabel = el('label', 'dlg-label dlg-location-label', 'Directory or manifest');
|
|
116
|
+
locationLabel.htmlFor = 'collection-dialog-location';
|
|
117
|
+
const location = el('input');
|
|
118
|
+
location.type = 'text';
|
|
119
|
+
location.id = 'collection-dialog-location';
|
|
120
|
+
location.autocomplete = 'off';
|
|
121
|
+
location.spellcheck = false;
|
|
122
|
+
// Short, because the label beside it already says what kind of
|
|
123
|
+
// directory or manifest is wanted.
|
|
124
|
+
location.placeholder = 'Absolute path or URL';
|
|
125
|
+
// Opens what the box names in a new tab: its directory listing,
|
|
126
|
+
// manifest or GitHub page. Enabled while the box holds something a
|
|
127
|
+
// link can be made from (`locationLink`).
|
|
128
|
+
const openLocation = el('button', 'dlg-open-location');
|
|
129
|
+
openLocation.type = 'button';
|
|
130
|
+
// Retitled per value (`locationNoun`).
|
|
131
|
+
setGlyphButton(openLocation, '#sm-open', 'Open in a new tab');
|
|
132
|
+
// Copies what the box names: a local location as its plain path (a
|
|
133
|
+
// `file://` URL loses the prefix), anything else as given.
|
|
134
|
+
const copyLocation = makeCopyButton({
|
|
135
|
+
label: 'Copy',
|
|
136
|
+
text: () => canonicalCollectionInput(location.value.trim()),
|
|
137
|
+
});
|
|
138
|
+
const locationRow = el('div', 'dlg-location');
|
|
139
|
+
locationRow.append(location, copyLocation, openLocation);
|
|
140
|
+
form.append(locationLabel, locationRow);
|
|
141
|
+
form.appendChild(el('span', 'dlg-label', 'Type'));
|
|
142
|
+
const typeRow = el('div', 'dlg-type');
|
|
143
|
+
const typeBubble = el('span', 'collection-type-bubble');
|
|
144
|
+
const typeHint = el('span', 'dlg-hint');
|
|
145
|
+
typeRow.append(typeBubble, typeHint);
|
|
146
|
+
form.appendChild(typeRow);
|
|
147
|
+
form.appendChild(el('span', 'dlg-label', 'Options'));
|
|
148
|
+
const localFilesLabel = el('label', 'dlg-check');
|
|
149
|
+
const localFiles = el('input');
|
|
150
|
+
localFiles.type = 'checkbox';
|
|
151
|
+
localFiles.id = 'collection-dialog-local-files';
|
|
152
|
+
const localText = el('span');
|
|
153
|
+
localText.appendChild(document.createTextNode('Allow local file overrides'));
|
|
154
|
+
const localFilesHint = el('span', 'dlg-hint');
|
|
155
|
+
localText.appendChild(localFilesHint);
|
|
156
|
+
localFilesLabel.append(localFiles, localText);
|
|
157
|
+
form.appendChild(localFilesLabel);
|
|
158
|
+
// The message and its optional action sit side by side in one row, so
|
|
159
|
+
// the offer and the reason for it read as one thing. The button is a
|
|
160
|
+
// SIBLING of the message rather than a child: `.dlg-error` is what
|
|
161
|
+
// every caller (and every test) reads the message from, and a nested
|
|
162
|
+
// button would show up in its text even while hidden.
|
|
163
|
+
const errorRow = el('div', 'dlg-error-row');
|
|
164
|
+
const error = el('p', 'dlg-error');
|
|
165
|
+
error.setAttribute('role', 'alert');
|
|
166
|
+
// Only the single-script verdict raises it.
|
|
167
|
+
const errorAction = el('button', 'dlg-error-action', 'Install script');
|
|
168
|
+
errorAction.type = 'button';
|
|
169
|
+
errorAction.hidden = true;
|
|
170
|
+
errorRow.append(error, errorAction);
|
|
171
|
+
dialog.appendChild(errorRow);
|
|
172
|
+
const actions = el('div', 'dlg-actions');
|
|
173
|
+
const cancel = el('button', undefined, 'Cancel');
|
|
174
|
+
cancel.type = 'button';
|
|
175
|
+
const ok = el('button', 'primary');
|
|
176
|
+
ok.type = 'submit';
|
|
177
|
+
ok.setAttribute('form', 'collection-dialog-form');
|
|
178
|
+
form.id = 'collection-dialog-form';
|
|
179
|
+
actions.append(cancel, ok);
|
|
180
|
+
dialog.appendChild(actions);
|
|
181
|
+
document.body.appendChild(dialog);
|
|
182
|
+
return {
|
|
183
|
+
dialog, form, title, locationLabel, locationRow, location, copyLocation, openLocation, name, typeBubble, typeHint,
|
|
184
|
+
localFilesLabel, localFiles, localFilesHint, error, errorAction, cancel, ok,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Open the dialog and resolve with what it made or changed, or null
|
|
188
|
+
// when it was dismissed. One at a time: a second call while one is
|
|
189
|
+
// open resolves null immediately.
|
|
190
|
+
export function openCollectionDialog(request) {
|
|
191
|
+
if (!elements)
|
|
192
|
+
elements = buildDialog();
|
|
193
|
+
const ui = elements;
|
|
194
|
+
if (dialogInFlight)
|
|
195
|
+
return Promise.resolve(null);
|
|
196
|
+
dialogInFlight = true;
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
void runDialog(ui, request, (outcome) => {
|
|
199
|
+
dialogInFlight = false;
|
|
200
|
+
resolve(outcome);
|
|
201
|
+
}).catch((err) => {
|
|
202
|
+
// Nothing in `runDialog` before `showModal` is expected to throw,
|
|
203
|
+
// but a rejection there would otherwise strand the caller AND
|
|
204
|
+
// leave the flag set, wedging every later open.
|
|
205
|
+
dialogInFlight = false;
|
|
206
|
+
resolve(null);
|
|
207
|
+
dlog(`[SourceMonkey] collection dialog failed to open: ${describeError(err)}`);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function runDialog(ui, request, resolve) {
|
|
212
|
+
const { mode, collection } = request;
|
|
213
|
+
const editing = mode === 'edit' ? collection ?? null : null;
|
|
214
|
+
// Everything else on the page, for the live "already exists" checks.
|
|
215
|
+
// A snapshot: a collection added from another tab while this dialog
|
|
216
|
+
// is open is caught by the store's own check on submit.
|
|
217
|
+
const others = (await getCollections()).filter((c) => c.id !== editing?.id);
|
|
218
|
+
// ---- state ----
|
|
219
|
+
// A location is off the table for a scripts-only create, and fixed
|
|
220
|
+
// (shown read-only, or absent) when editing.
|
|
221
|
+
const editingScripts = !!editing && collectionKind(editing.path) === 'scripts';
|
|
222
|
+
const showLocation = !request.scriptsOnly && !editingScripts;
|
|
223
|
+
// The empty-directory warning blocks once; the second Create goes
|
|
224
|
+
// through.
|
|
225
|
+
let warnedEmpty = false;
|
|
226
|
+
let settled = false;
|
|
227
|
+
let probeTimer;
|
|
228
|
+
// The opening paint. Its location came from the Add field, not from
|
|
229
|
+
// typing, so it is classified straight away rather than after the
|
|
230
|
+
// keystroke debounce.
|
|
231
|
+
let firstPaint = true;
|
|
232
|
+
const finish = (outcome) => {
|
|
233
|
+
if (settled)
|
|
234
|
+
return;
|
|
235
|
+
settled = true;
|
|
236
|
+
// A pending classification has nothing left to update.
|
|
237
|
+
if (probeTimer !== undefined)
|
|
238
|
+
clearTimeout(probeTimer);
|
|
239
|
+
if (ui.dialog.open)
|
|
240
|
+
ui.dialog.close();
|
|
241
|
+
resolve(outcome);
|
|
242
|
+
};
|
|
243
|
+
// ---- fill ----
|
|
244
|
+
const wording = CREATE_WORDING[request.createPurpose ?? 'collectionOnly'];
|
|
245
|
+
ui.title.textContent = editing ? 'Edit collection' : wording.title;
|
|
246
|
+
ui.ok.textContent = editing ? 'Save' : wording.ok;
|
|
247
|
+
ui.location.value = editing ? locationFieldValue(editing.path) : request.prefill?.location ?? '';
|
|
248
|
+
ui.location.disabled = !!editing;
|
|
249
|
+
// Editing, the location is fixed, so its label names the one it is.
|
|
250
|
+
// Adding, it offers both, over two lines to keep the column narrow.
|
|
251
|
+
if (!editing)
|
|
252
|
+
ui.locationLabel.replaceChildren('Directory or ', el('br'), 'manifest');
|
|
253
|
+
else
|
|
254
|
+
ui.locationLabel.textContent = collectionKind(editing.path) === 'manifest' ? 'Manifest' : 'Directory';
|
|
255
|
+
ui.locationLabel.hidden = !showLocation;
|
|
256
|
+
ui.location.hidden = !showLocation;
|
|
257
|
+
ui.locationRow.hidden = !showLocation;
|
|
258
|
+
// Blank for a new location collection: the inferred name is what a
|
|
259
|
+
// blank means, and echoing the path into the field would only invite
|
|
260
|
+
// editing a value that isn't the user's.
|
|
261
|
+
ui.name.value = editing
|
|
262
|
+
? editing.displayName?.trim() || inferDisplayName(editing.path)
|
|
263
|
+
: request.prefill?.name ?? '';
|
|
264
|
+
ui.localFiles.checked = editing ? collectionUsesLocalFiles(editing) : true;
|
|
265
|
+
const setError = (text, warn = false) => {
|
|
266
|
+
ui.error.textContent = text;
|
|
267
|
+
ui.error.classList.toggle('warn', warn);
|
|
268
|
+
};
|
|
269
|
+
// The typed location in the one spelling the store will hold, so far
|
|
270
|
+
// as can be told without a network call.
|
|
271
|
+
const canonicalLoc = () => canonicalCollectionInput(ui.location.value.trim());
|
|
272
|
+
// Verdicts already reached, keyed by the exact string typed, plus the
|
|
273
|
+
// debounce that fetches the next one. Declared above `evaluate`,
|
|
274
|
+
// which calls it: a `const` arrow used before its line is a TDZ trap
|
|
275
|
+
// for whoever adds the next caller.
|
|
276
|
+
const verdicts = new Map();
|
|
277
|
+
let probingFor = null;
|
|
278
|
+
// The verdict for `loc`, or null while we are still finding out —
|
|
279
|
+
// in which case a probe is scheduled and `refresh` runs again when it
|
|
280
|
+
// lands.
|
|
281
|
+
//
|
|
282
|
+
// `now` skips the debounce for a location the user didn't type here
|
|
283
|
+
// (an Add-field prefill), which is already complete and would
|
|
284
|
+
// otherwise show `checking` for a quarter second on every add.
|
|
285
|
+
const verdictFor = (loc, now = false) => {
|
|
286
|
+
const known = verdicts.get(loc);
|
|
287
|
+
if (known)
|
|
288
|
+
return known;
|
|
289
|
+
if (probingFor !== loc) {
|
|
290
|
+
probingFor = loc;
|
|
291
|
+
if (probeTimer !== undefined)
|
|
292
|
+
clearTimeout(probeTimer);
|
|
293
|
+
const run = () => {
|
|
294
|
+
void classifyLocation(loc)
|
|
295
|
+
// A classification that threw is still a classification —
|
|
296
|
+
// whatever went wrong is what's wrong with this location.
|
|
297
|
+
.catch((err) => ({ kind: 'invalid', message: describeError(err) }))
|
|
298
|
+
.then((verdict) => {
|
|
299
|
+
verdicts.set(loc, verdict);
|
|
300
|
+
if (!settled)
|
|
301
|
+
refresh();
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
if (now)
|
|
305
|
+
run();
|
|
306
|
+
else
|
|
307
|
+
probeTimer = setTimeout(run, LOCATION_PROBE_DELAY_MS);
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
};
|
|
311
|
+
// Every edit forgets the refusals. A location that couldn't be read
|
|
312
|
+
// may become readable while the dialog is open — the user creates the
|
|
313
|
+
// directory, or turns file access on — and a cached refusal would
|
|
314
|
+
// outlast the fix, so retyping the path is the retry. Only refusals:
|
|
315
|
+
// re-reading a directory we already listed would just cost a scan.
|
|
316
|
+
const forgetRefusals = () => {
|
|
317
|
+
for (const [loc, verdict] of verdicts) {
|
|
318
|
+
if (verdict.kind === 'invalid')
|
|
319
|
+
verdicts.delete(loc);
|
|
320
|
+
}
|
|
321
|
+
probingFor = null;
|
|
322
|
+
};
|
|
323
|
+
// What's wrong with the typed name, or null. Blank is never wrong
|
|
324
|
+
// here: for a location collection it means "use the inferred name",
|
|
325
|
+
// and for a scripts collection the caller checks it separately (the
|
|
326
|
+
// Type row says what a nameless, locationless form still needs).
|
|
327
|
+
const nameProblem = (inferred) => {
|
|
328
|
+
const name = ui.name.value.trim();
|
|
329
|
+
if (!name)
|
|
330
|
+
return null;
|
|
331
|
+
// The row's own path is what earns an exemption from the reserved
|
|
332
|
+
// names — a built-in default keeps the name it owns.
|
|
333
|
+
if (!mayUseReservedName(name, editing ? editing.path : null))
|
|
334
|
+
return RESERVED_COLLECTION_NAME;
|
|
335
|
+
// Only a name the user chose is held unique — the same test
|
|
336
|
+
// `createLocationCollection` runs, against the same canonical
|
|
337
|
+
// spelling, so the two can't disagree about whether a typed name
|
|
338
|
+
// counts as chosen. (A github URL the store would still rewrite via
|
|
339
|
+
// its API is the one case they can: that rewrite needs a network
|
|
340
|
+
// round trip, so the store's own check stays the backstop.)
|
|
341
|
+
if (name !== inferred && collectionNameInUse(name, others)) {
|
|
342
|
+
return collectionNameInUseMessage(name);
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
};
|
|
346
|
+
// Runs on every keystroke; Create/Save is disabled while it says so.
|
|
347
|
+
const evaluate = () => {
|
|
348
|
+
const blank = { hint: '', message: '', field: null, ready: true, scriptLocation: '' };
|
|
349
|
+
if (editing) {
|
|
350
|
+
// Type and location are fixed, so only the name can be wrong.
|
|
351
|
+
// A blank name means "use the inferred one" — but a scripts
|
|
352
|
+
// collection has no location to infer from, so for that one blank
|
|
353
|
+
// is simply missing, exactly as it is when creating.
|
|
354
|
+
const inferred = inferDisplayName(editing.path);
|
|
355
|
+
const message = nameProblem(inferred);
|
|
356
|
+
const missing = !message && !inferred && !ui.name.value.trim();
|
|
357
|
+
return {
|
|
358
|
+
...blank,
|
|
359
|
+
bubble: collectionBubbleKey(editing.path),
|
|
360
|
+
hint: missing ? enterSomethingHint(false) : '',
|
|
361
|
+
message: message ?? '',
|
|
362
|
+
field: message ? 'name' : null,
|
|
363
|
+
ready: !message && !missing,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const loc = showLocation ? ui.location.value.trim() : '';
|
|
367
|
+
const name = ui.name.value.trim();
|
|
368
|
+
if (!loc) {
|
|
369
|
+
// No location: a collection of individually installed scripts,
|
|
370
|
+
// which is nothing at all until it is named.
|
|
371
|
+
if (!name) {
|
|
372
|
+
return { ...blank, bubble: INVALID_BUBBLE_KEY, hint: enterSomethingHint(showLocation), ready: false };
|
|
373
|
+
}
|
|
374
|
+
const message = nameProblem('');
|
|
375
|
+
return {
|
|
376
|
+
...blank,
|
|
377
|
+
bubble: SCRIPTS_BUBBLE_KEY,
|
|
378
|
+
message: message ?? '',
|
|
379
|
+
field: message ? 'name' : null,
|
|
380
|
+
ready: !message,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const verdict = verdictFor(loc, firstPaint);
|
|
384
|
+
if (!verdict)
|
|
385
|
+
return { ...blank, bubble: CHECKING_BUBBLE_KEY, ready: false };
|
|
386
|
+
if (verdict.kind === 'script') {
|
|
387
|
+
return {
|
|
388
|
+
...blank,
|
|
389
|
+
bubble: INVALID_BUBBLE_KEY,
|
|
390
|
+
message: SINGLE_SCRIPT_NOT_A_COLLECTION,
|
|
391
|
+
field: 'location',
|
|
392
|
+
ready: false,
|
|
393
|
+
scriptLocation: loc,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (verdict.kind === 'invalid') {
|
|
397
|
+
return { ...blank, bubble: INVALID_BUBBLE_KEY, message: verdict.message, field: 'location', ready: false };
|
|
398
|
+
}
|
|
399
|
+
// The same key `collectionBubbleKey` builds for a stored row, but
|
|
400
|
+
// from the verdict rather than the path: the two agree on the
|
|
401
|
+
// scheme half and can differ on the other, because this one knows
|
|
402
|
+
// what was actually read where that one only has a filename.
|
|
403
|
+
const bubble = `${inferCollectionType(loc)}-${verdict.kind === 'manifest' ? 'manifest' : 'dir'}`;
|
|
404
|
+
if (others.some((c) => canonicalCollectionInput(c.path) === canonicalLoc())) {
|
|
405
|
+
return { ...blank, bubble, message: COLLECTION_ALREADY_REGISTERED, field: 'location', ready: false };
|
|
406
|
+
}
|
|
407
|
+
// Name problems are reported only once the location is sound: the
|
|
408
|
+
// location is the field being filled in, and marking Name while the
|
|
409
|
+
// location is unusable points at the wrong one.
|
|
410
|
+
const message = nameProblem(inferDisplayName(canonicalLoc()));
|
|
411
|
+
return {
|
|
412
|
+
...blank,
|
|
413
|
+
bubble,
|
|
414
|
+
message: message ?? '',
|
|
415
|
+
field: message ? 'name' : null,
|
|
416
|
+
ready: !message,
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
const refresh = () => {
|
|
420
|
+
const state = evaluate();
|
|
421
|
+
const loc = showLocation ? ui.location.value.trim() : '';
|
|
422
|
+
const supportsLocal = !loc || collectionSupportsLocalFiles(loc);
|
|
423
|
+
ui.localFiles.disabled = !supportsLocal;
|
|
424
|
+
ui.localFilesLabel.classList.toggle('off', !supportsLocal);
|
|
425
|
+
ui.localFilesHint.textContent = supportsLocal ? `: ${LOCAL_FILES_HINT}` : `: ${localFilesUnavailableHint(loc)}`;
|
|
426
|
+
// The placeholder is the field's own explanation of what blank
|
|
427
|
+
// means — required for a scripts collection, "use the inferred
|
|
428
|
+
// name" once a location says what the collection is.
|
|
429
|
+
ui.name.placeholder = !showLocation || !loc
|
|
430
|
+
? COLLECTION_NAME_REQUIRED
|
|
431
|
+
: namePlaceholderFor(state.bubble);
|
|
432
|
+
ui.typeBubble.className = `collection-type-bubble bubble-${state.bubble}`;
|
|
433
|
+
ui.typeBubble.textContent = state.bubble;
|
|
434
|
+
ui.typeBubble.title = typeBubbleTitle(state.bubble);
|
|
435
|
+
ui.typeHint.textContent = state.hint;
|
|
436
|
+
ui.location.classList.toggle('invalid', state.field === 'location');
|
|
437
|
+
ui.name.classList.toggle('invalid', state.field === 'name');
|
|
438
|
+
setError(state.message);
|
|
439
|
+
ui.errorAction.hidden = !state.scriptLocation;
|
|
440
|
+
ui.errorAction.onclick = state.scriptLocation
|
|
441
|
+
// A new tab: the dialog's caller (usually the collections page)
|
|
442
|
+
// stays where it was, so backing out of the install lands the
|
|
443
|
+
// user where they started.
|
|
444
|
+
? () => { void openExtensionTab(chrome.runtime.getURL(previewUrl(state.scriptLocation))); }
|
|
445
|
+
: null;
|
|
446
|
+
ui.ok.disabled = !state.ready;
|
|
447
|
+
};
|
|
448
|
+
// ---- wiring (replaced on every open, so stale closures can't fire) ----
|
|
449
|
+
const syncOpenLocation = () => {
|
|
450
|
+
ui.openLocation.disabled = locationLink(ui.location.value) === '';
|
|
451
|
+
ui.copyLocation.disabled = ui.location.value.trim() === '';
|
|
452
|
+
// Named by what the box holds: "Copy filename", "Open URL in a new tab".
|
|
453
|
+
const noun = locationNoun(ui.location.value);
|
|
454
|
+
setGlyphButtonLabel(ui.openLocation, noun ? `Open ${noun.open} in a new tab` : 'Open in a new tab');
|
|
455
|
+
const copyLabel = noun ? `Copy ${noun.copy}` : 'Copy';
|
|
456
|
+
ui.copyLocation.title = copyLabel;
|
|
457
|
+
ui.copyLocation.setAttribute('aria-label', copyLabel);
|
|
458
|
+
};
|
|
459
|
+
syncOpenLocation();
|
|
460
|
+
ui.openLocation.onclick = () => {
|
|
461
|
+
const link = locationLink(ui.location.value);
|
|
462
|
+
if (!link)
|
|
463
|
+
return;
|
|
464
|
+
openExtensionTab(link).catch((err) => {
|
|
465
|
+
dlog(`[SourceMonkey] could not open ${link}: ${describeError(err)}`);
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
ui.location.oninput = () => {
|
|
469
|
+
warnedEmpty = false;
|
|
470
|
+
forgetRefusals();
|
|
471
|
+
syncOpenLocation();
|
|
472
|
+
refresh();
|
|
473
|
+
};
|
|
474
|
+
ui.name.oninput = () => refresh();
|
|
475
|
+
ui.localFiles.onchange = () => refresh();
|
|
476
|
+
ui.cancel.onclick = () => finish(null);
|
|
477
|
+
ui.dialog.onclose = () => finish(null);
|
|
478
|
+
ui.form.onsubmit = (event) => {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
void submit();
|
|
481
|
+
};
|
|
482
|
+
const busy = async (work) => {
|
|
483
|
+
ui.ok.disabled = true;
|
|
484
|
+
ui.cancel.disabled = true;
|
|
485
|
+
const spinner = el('span', 'busy-spinner');
|
|
486
|
+
ui.ok.after(spinner);
|
|
487
|
+
try {
|
|
488
|
+
return await work();
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
spinner.remove();
|
|
492
|
+
ui.cancel.disabled = false;
|
|
493
|
+
// Live again even when the slot now shows a failure, deliberately:
|
|
494
|
+
// the empty-directory warning is a "click Create again" prompt,
|
|
495
|
+
// and a probe that failed on a network blip deserves a retry
|
|
496
|
+
// without retyping. So the "disabled while `evaluate` says so"
|
|
497
|
+
// invariant holds only up to the first submit.
|
|
498
|
+
ui.ok.disabled = false;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const submit = async () => {
|
|
502
|
+
refresh();
|
|
503
|
+
if (ui.ok.disabled)
|
|
504
|
+
return;
|
|
505
|
+
const name = ui.name.value.trim();
|
|
506
|
+
const localFiles = ui.localFiles.disabled ? undefined : ui.localFiles.checked;
|
|
507
|
+
const loc = showLocation ? ui.location.value.trim() : '';
|
|
508
|
+
try {
|
|
509
|
+
if (editing) {
|
|
510
|
+
const priorLocal = collectionUsesLocalFiles(editing);
|
|
511
|
+
const updated = await busy(() => setCollectionOptions(editing.id, {
|
|
512
|
+
displayName: name,
|
|
513
|
+
...(localFiles === undefined ? {} : { localFiles }),
|
|
514
|
+
}));
|
|
515
|
+
if (!updated) {
|
|
516
|
+
setError(COLLECTION_GONE_HERE);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
finish({
|
|
520
|
+
collection: updated,
|
|
521
|
+
created: false,
|
|
522
|
+
localFilesChanged: localFiles !== undefined && localFiles !== priorLocal,
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (!loc) {
|
|
527
|
+
// Only a location collection can come back null (a duplicate
|
|
528
|
+
// location); the scripts branch throws on the one thing it
|
|
529
|
+
// refuses, a name already in use. A null here would be a store
|
|
530
|
+
// bug, so it says so rather than guessing at a cause.
|
|
531
|
+
const result = await busy(() => createCollection({ kind: 'scripts', name, localFiles }));
|
|
532
|
+
if (!result) {
|
|
533
|
+
setError('Internal error: the collection was not created');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
finish({ collection: result.collection, created: true, localFilesChanged: false });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// Read before writing: a location that can't be listed is
|
|
540
|
+
// refused here, with the reason, rather than becoming a section
|
|
541
|
+
// whose only content is a scan error. Empty is allowed, but said
|
|
542
|
+
// once first.
|
|
543
|
+
const probe = await busy(() => probeCollectionLocation(loc));
|
|
544
|
+
if (probe.scriptCount === 0 && !warnedEmpty) {
|
|
545
|
+
warnedEmpty = true;
|
|
546
|
+
setError(EMPTY_LOCATION_WARNING, true);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const result = await busy(() => createCollection({ kind: 'location', path: loc, name, localFiles }));
|
|
550
|
+
if (!result) {
|
|
551
|
+
setError(COLLECTION_ALREADY_REGISTERED);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
finish({ collection: result.collection, created: true, localFilesChanged: false });
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
setError(describeError(err));
|
|
558
|
+
// A create that failed on the location — the probe, or the store's
|
|
559
|
+
// own dedup — is about that field.
|
|
560
|
+
if (!editing && loc)
|
|
561
|
+
ui.location.classList.add('invalid');
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
refresh();
|
|
565
|
+
firstPaint = false;
|
|
566
|
+
ui.dialog.showModal();
|
|
567
|
+
// Where typing starts: the location for a new collection that has
|
|
568
|
+
// none yet, the name otherwise.
|
|
569
|
+
if (showLocation && !editing && !ui.location.value)
|
|
570
|
+
ui.location.focus();
|
|
571
|
+
else
|
|
572
|
+
ui.name.focus();
|
|
573
|
+
}
|
|
574
|
+
// Edit-mode message for a row that vanished under the dialog: the
|
|
575
|
+
// shared string, wrapped with what to do about it here.
|
|
576
|
+
const COLLECTION_GONE_HERE = `${COLLECTION_GONE}. Close this dialog and check the page.`;
|
|
577
|
+
// What a blank Name means, once a location has said what kind of
|
|
578
|
+
// collection this is. The `checking` / `invalid` states have no kind to
|
|
579
|
+
// name yet, so they get the bare form.
|
|
580
|
+
function namePlaceholderFor(bubbleKey) {
|
|
581
|
+
const kind = bubbleKey.endsWith('-manifest') ? 'manifest'
|
|
582
|
+
: bubbleKey.endsWith('-dir') ? 'directory'
|
|
583
|
+
: null;
|
|
584
|
+
return kind ? `Name is optional for ${kind} collections` : 'Name is optional';
|
|
585
|
+
}
|
|
586
|
+
// What the (read-only) Location field shows for an existing row: the
|
|
587
|
+
// stored path, which is already the canonical spelling. A container
|
|
588
|
+
// has none. Not `displayLocation` — `script-display.ts` exports that
|
|
589
|
+
// name for a different job (a script's own location, decoded).
|
|
590
|
+
function locationFieldValue(path) {
|
|
591
|
+
return collectionKind(path) === 'scripts' ? '' : path;
|
|
592
|
+
}
|
|
593
|
+
// What the location box names, as a link a tab can open: an http(s) or
|
|
594
|
+
// file:// URL as given, an absolute path as a file:// URL. '' for
|
|
595
|
+
// anything else, which disables the button.
|
|
596
|
+
function locationLink(value) {
|
|
597
|
+
const v = value.trim();
|
|
598
|
+
if (isHttpUrl(v) || isFileUrl(v)) {
|
|
599
|
+
try {
|
|
600
|
+
return new URL(v).href;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return '';
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return v.startsWith('/') ? pathToFileUrl(v) : '';
|
|
607
|
+
}
|
|
608
|
+
// What the location box holds, in the words its buttons use: a URL, or
|
|
609
|
+
// a local file (a manifest) or directory. Null when it's empty.
|
|
610
|
+
function locationNoun(value) {
|
|
611
|
+
const v = value.trim();
|
|
612
|
+
if (!v)
|
|
613
|
+
return null;
|
|
614
|
+
if (isHttpUrl(v))
|
|
615
|
+
return { copy: 'URL', open: 'URL' };
|
|
616
|
+
if (isManifestCollection(v))
|
|
617
|
+
return { copy: 'filename', open: 'file' };
|
|
618
|
+
return { copy: 'directory', open: 'directory' };
|
|
619
|
+
}
|