|
| 1 | +--- |
| 2 | +title: "AddDocumentStartJavaScript" |
| 3 | +description: "How we use AddDocumentStartJavaScript" |
| 4 | +keywords: ["adddocumentstartjavascript", "addDocumentStartJavaScript", "AddDocumentStartJavaScript", "add document start javascript", "css", "c-s-s", "CSS", "C-S-S", "WebView", "webview", "contentscopescripts", "content-scope-scripts", "ContentScopeScripts", "contentScopeScripts"] |
| 5 | +alwaysApply: false |
| 6 | +--- |
| 7 | + |
| 8 | +# Adding a new script using `addDocumentStartJavaScript` |
| 9 | + |
| 10 | +There are 2 ways of adding a new script using |
| 11 | +1. (Recommended) Using `AddDocumentStartScriptDelegate` to implement `AddDocumentStartJavaScript` via delegation pattern. This approach already takes care of |
| 12 | +preventing adding the same script more than once, and removing the previous script and adding a new one if needed. It also adds checks on the WebView lifecycle to minimize native crashes. |
| 13 | +2. (Manual approach, only recommended if more flexibility than the one provided by the delegate is needed) Manually implementing `AddDocumentStartJavaScript` |
| 14 | + |
| 15 | +## Using `AddDocumentStartScriptDelegate` to implement `AddDocumentStartJavaScript` via delegation pattern |
| 16 | +``` |
| 17 | +class <YourFeature>AddDocumentStartJavaScript @Inject constructor( |
| 18 | + scriptInjectorDelegate: AddDocumentStartScriptDelegate, |
| 19 | +) : AddDocumentStartJavaScript by scriptInjectorDelegate.createPlugin( |
| 20 | + object : AddDocumentStartJavaScriptScriptStrategy { |
| 21 | + override suspend fun canInject(): Boolean { |
| 22 | + TODO("Implement logic to determine if the script can be added (i.e. checking RC flags or user settings)" + |
| 23 | + "or return true if always applicable") |
| 24 | + } |
| 25 | +
|
| 26 | + override suspend fun getScriptString(): String { |
| 27 | + TODO("Return the script to be injected") |
| 28 | + } |
| 29 | +
|
| 30 | + override val allowedOriginRules: Set<String> |
| 31 | + get() = TODO("Return the set of allowed origin rules. For example:" + |
| 32 | + "- if the script should be injected on all origins, return setOf(\"*\")" + |
| 33 | + "- if the script should be injected only on specific origins, return setOf(\"https://example.com\", \"https://another.com\")" + |
| 34 | + "- if the script should be injected on all subdomains of a domain, return setOf(\"https://*.example.com\")") |
| 35 | +
|
| 36 | + override val context: String |
| 37 | + get() = TODO("Return a string representing the context of this script, e.g., \"YourFeature\"") |
| 38 | + }, |
| 39 | +) |
| 40 | +``` |
| 41 | + |
| 42 | +## Manually implementing `AddDocumentStartJavaScript` |
| 43 | +Since the `AddDocumentStartScriptDelegate` already solves most of the issues and dangers of working with the `addDocumentStartJavaScript` API, manual implementation isn't recommended. If absolutely necessary, having a look at `RealAddDocumentStartScriptDelegate` is recommended, in order to replicate some best practices: |
| 44 | +* If a script has already been added, don't add it again unless the content has changed |
| 45 | +* If the content has changed, remove the previous `ScriptHandler` before adding the new script. Call `remove` on the main thread |
| 46 | +* Use `WebViewCompatWrapper` instead of calling `WebViewCompat` directly, as it includes several checks on the `WebView` lifecycle and ensures proper threading is used |
| 47 | + |
| 48 | +# Adding a new script to the browser (DuckDuckGoWebView/BrowserTabFragment) |
| 49 | + |
| 50 | +If you need your script to be executed on the main browser WebView, you need to create a browser plugin that wraps your `AddDocumentStartJavaScript` implementation. |
| 51 | + |
| 52 | +## Step 1: Create the core implementation |
| 53 | + |
| 54 | +Follow the patterns described in the [delegation pattern section](#using-adddocumentstartscriptdelegate-to-implement-adddocumentstartjavascript-via-delegation-pattern) above. |
| 55 | + |
| 56 | +## Step 2: Create the browser plugin wrapper |
| 57 | + |
| 58 | +```kotlin |
| 59 | +@ContributesMultibinding(FragmentScope::class) |
| 60 | +class <YourFeature>AddDocumentStartJavaScriptBrowserPlugin @Inject constructor( |
| 61 | + private val <yourFeature>AddDocumentStartJavaScript: <YourFeature>AddDocumentStartJavaScript, |
| 62 | +) : AddDocumentStartJavaScriptBrowserPlugin { |
| 63 | + override fun addDocumentStartJavaScript(): AddDocumentStartJavaScript = |
| 64 | + <yourFeature>AddDocumentStartJavaScript |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +## How it works |
| 69 | + |
| 70 | +The browser automatically calls your script's `addDocumentStartJavaScript(webView)` method when: |
| 71 | +- A new page loads |
| 72 | +- If you also need it to be called when Privacy protections are updated, you need to add your implementation context to `BrowserTabViewModel#privacyProtectionsUpdated` |
| 73 | +- If you need your script to be re-added in any other circumstances, follow the same pattern as `BrowserTabViewModel#privacyProtectionsUpdated`, so we only update the necessary scripts, not all of them. However, this is strongly discouraged by the Chromium team, and should only be done if messaging is not viable (for example, out of performance concerns) |
| 74 | + |
| 75 | +The `AddDocumentStartScriptDelegate` handles lifecycle management, script deduplication, and WebView safety checks. |
| 76 | + |
| 77 | +## Important Notes |
| 78 | + |
| 79 | +- Use `FragmentScope::class` for browser-specific plugins |
| 80 | +- Ensure your core implementation is properly injected with `@SingleInstanceIn(FragmentScope::class)` |
| 81 | +- The `context` string should be unique and descriptive |
0 commit comments