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.
Files changed (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. 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());