diff --git a/docs/entrypoints/content-scripts.md b/docs/entrypoints/content-scripts.md index 8ce266e7..f05a545c 100644 --- a/docs/entrypoints/content-scripts.md +++ b/docs/entrypoints/content-scripts.md @@ -137,199 +137,3 @@ export default defineContentScript({ }, }); ``` - -## UI - -WXT provides a utility function, `createContentScriptUi` to simplify mounting a UI from a content script. Internally, it uses the `ShadowRoot` API to isolate your CSS from the webpages. - -`createContentScriptUi` requires a `ContentScriptContext` so that when the context is invalidated, the UI is automatically removed from the webpage. - -:::details When to use `createContentScriptUi` -You should only use `createContentScriptUi` if you want your UI's styles isolated from the webpages. If you want to create a more "integrated" UI that uses the page's styles, you can just use the regular JS API's to append your UI to the page. - -```ts -const ui = document.createElement('div'); -const anchor = document.querySelector('#anchor-selector'); -anchor.append(ui); -``` - -You can try out [`createContentScriptIframe`](#createcontentscriptiframe) as an alternative solution - -::: - -### Usage - -To use `createContentScriptUi`, follow these steps: - -1. Import your CSS file at the top of your content script -2. Set `cssInjectionMode: "ui"` inside `defineContentScript` -3. Call `createContentScriptUi` -4. Call `mount` to add the UI to the webpage - -Here's a basic example: - -```ts -// entrypoints/ui.content/index.ts -import './style.css'; - -export default defineContentScript({ - // ... - cssInjectionMode: 'ui', - - async main(ctx) { - const ui = await createContentScriptUi(ctx, { - name: 'example-ui', - type: 'inline', - anchor: '#some-element', - append: 'after', - mount(container) { - // Mount UI inside `container`... - }, - }); - - // You must call `mount` to add the UI to the page. - ui.mount(); - }, -}); -``` - -If you're using a frontend framework, you'll also need to include an `onRemoved` callback: - -:::code-group - -```ts [Vue] -import { createApp } from 'vue'; - -createContentScriptUi(ctx, { - // ... - mount(container) { - // Create a new app and mount it inside the container - const app = createApp(...); - app.mount(container); - return app; - }, - onRemove(app) { - // When the UI is removed from the DOM, call unmount to stop the app - app.unmount(); - }, -}); -``` - -```ts [React] -import ReactDOM from 'react-dom/client'; - -createContentScriptUi(ctx, { - // ... - mount(container) { - // Create a root using the container and render your app - const root = ReactDOM.createRoot(container); - root.render(...); - return root; - }, - onRemove(root) { - // When the UI is removed from the DOM, call unmount to stop the app - root.unmount(); - }, -}); -``` - -```ts [Svelte] -import App from './App.svelte'; - -createContentScriptUi(ctx, { - // ... - mount(container) { - // Mount your app component inside the container - return new App({ - target: container, - }); - }, - onRemove(app) { - // When the UI is removed from the DOM, call $destroy to stop the app - app.$destroy(); - }, -}); -``` - -```ts [Solid] -import { render } from 'solid-js/web'; - -createContentScriptUi(ctx, { - // ... - mount(container) { - // Render your app component into the container - return render(() => ..., container) - }, - onRemove(unmount) { - // When the UI is removed from the DOM, call unmount to stop the app - unmount(); - }, -}); -``` - -::: - -### `anchor` - -The anchor dictates where the UI will be mounted. - -### `append` - -Customize where the UI get's appended to the DOM, relative to the `anchor` element. - -### `type` - -There are 3 types of UI's you can mount. - -- `inline`: Shows up inline based on the `anchor` and `append` options -- `overlay`: Shows up inline, but styled to be 0px by 0px, with overflow visible. This causes the UI to overlay on top of the webpage's content -- `modal`: A fullscreen overlay that covers the entire screen, regardless of where it's anchored. - -> TODO: Add visualization of the different UI types. - -### Overlay `alignment` - -Because the overlay UI type results in a 0px by 0px container being added to the webpage, the `alignment` option allows you to configure which corner of your UI is aligned with the 0x0 element. - -> TODO: Add visualization of the different alignments. - -## IFrame - -WXT provides a utility function, `createContentScriptIframe` to simplify mounting a UI from a content script. It creates an iframe to an unlisted HTML page. Unlike `createContentScriptUi`, this API support HMR. - -`createContentScriptIframe` requires a `ContentScriptContext` so that when the context is invalidated, the UI is automatically removed from the webpage. - -### Usage - -To use `createContentScriptIframe`, follow these steps: - -1. Create an unlisted HTML page that will be loaded into your iframe -1. Add unlisted page to the manifest's `web_accessible_resouces` -1. Call `createContentScriptIframe` -1. Call `mount` to add the UI to the webpage - -Here's a basic example: - -```ts -export default defineContentScript({ - // ... - - async main(ctx) { - const ui = await createContentScriptIframe(ctx, { - page: '/your-unlisted-page.html', - type: 'inline', - anchor: '#some-element', - append: 'after', - }); - - // You must call `mount` to add the UI to the page. - ui.mount(); - }, -}); -``` - -The options, other than `page`, are the same as [`createContentScriptUi`](#anchor). - -### `page` - -The HTML page you want to load inside the iframe. This string will be passed into `browser.runtime.getURL` to resolve the full path of to your HTML page. diff --git a/docs/guide/content-script-ui.md b/docs/guide/content-script-ui.md new file mode 100644 index 00000000..980ad4d3 --- /dev/null +++ b/docs/guide/content-script-ui.md @@ -0,0 +1,326 @@ +# Content Script UI + +There are three ways to mount a UI inside a content script: + +[[toc]] + +Each has their own set of advantages and disadvantages. + +| Method | Isolated Styles | HMR | Use page's context | +| ---------- | :-------------: | :-: | :----------------: | +| Integrated | ❌ | ❌ | ✅ | +| ShadowRoot | ✅ | ❌ | ✅ | +| IFrame | ✅ | ✅ | ❌ | + +## Integrated + +Integrated content script UIs use the page's CSS to inject a UI that looks like it's apart of the page. + +WXT doesn't provide any utils for mounting integrated UIs yet. Here are some examples for setting up integrated UIs. + +:::code-group + +```ts [Vanilla] +// entrypoints/example-ui.content.ts +export default defineContentScript({ + main(ctx) { + // Create the UI container + const container = document.createElement('div'); + + // Add UI container to the page + const anchor = document.querySelector('#anchor'); + anchore.append(container); + + // Remove UI container when invalidated + ctx.onInvalidated(() => { + container.remove(); + }); + }, +}); +``` + +```ts [Vue] +// entrypoints/example-ui.content/index.ts +import { createApp } from 'vue'; + +export default defineContentScript({ + main(ctx) { + // Create the UI container + const container = document.createElement('div'); + + // Create the app and mount it to the UI container + const app = createApp(...); + app.mount(container); + + // Add UI container to the page + const anchor = document.querySelector('#anchor'); + anchore.append(container); + + // Unmount the app and remove UI container when invalidated + ctx.onInvalidated(() => { + app.unmount(); + container.remove(); + }); + }, +}); +``` + +```tsx [React] +// entrypoints/example-ui.content/index.tsx +import ReactDOM from 'react-dom/client'; + +export default defineContentScript({ + main(ctx) { + // Create the UI container + const container = document.createElement('div'); + + // Create a root on the UI container and render a component + const root = ReactDOM.createRoot(container); + root.render(...); + + // Add UI container to the page + const anchor = document.querySelector('#anchor'); + anchore.append(container); + + // Unmount the root and remove UI container when invalidated + ctx.onInvalidated(() => { + root.unmount(); + container.remove(); + }); + }, +}); +``` + +```ts [Svelete] +// entrypoints/example-ui.content/index.ts +import App from './App.svelte'; + +export default defineContentScript({ + main(ctx) { + // Create the UI container + const container = document.createElement('div'); + + // Create the Svelte app inside the UI container + const app = new App({ + target: ui, + }); + + // Add UI container to the page + const anchor = document.querySelector('#anchor'); + anchore.append(container); + + // Destroy the app and remove UI container when invalidated + ctx.onInvalidated(() => { + app.$destroy(); + container.remove(); + }); + }, +}); +``` + +```tsx [Solid] +// entrypoints/example-ui.content/index.ts +import { render } from 'solid-js/web'; + +export default defineContentScript({ + main(ctx) { + // Create the UI container + const container = document.createElement('div'); + + // Render your app to the UI container + const unmount = render(() => ..., container) + + // Add UI container to the page + const anchor = document.querySelector('#anchor'); + anchore.append(container); + + // Unmount the app and remove UI container when invalidated + ctx.onInvalidated(() => { + unmount(); + container.remove(); + }); + }, +}); +``` + +::: + +## ShadowRoot + +Often in web extensions, you don't want your content script's CSS effecting the page, or vise-versa. The [`ShadowRoot` API](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) is ideal for this. It isolates an element's style from the page's style. + +WXT provides a helper function, [`createContentScriptUi`](/api/wxt/client/functions/createContentScriptUi), that abstracts all the `ShadowRoot` setup away, making it easy to create UIs with isolated styles. + +To use `createContentScriptUi`, follow these steps: + +1. Import your CSS file at the top of your content script +2. Set `cssInjectionMode: "ui"` inside `defineContentScript` +3. Define your UI with `createContentScriptUi()` +4. Mount the UI so it is visible to users + +```ts +// 1. Import the style +import './style.css'; + +export default defineContentScript({ + // 2. Set cssInjectionMode + cssInjectionMode: 'ui', + + async main(ctx) { + // 3. Define your UI + const ui = await createContentScriptUi(ctx, { + name: 'example-ui', + anchor: '#anchor', + type: 'inline', + mount(container) { + // Define how your UI will be mounted inside the container + const app = document.createElement('p'); + app.textContent = 'Hello world!'; + container.append(app); + }, + }); + + // 4. Mount the UI + ui.mount(); + }, +}); +``` + +> `createContentScriptUi` will automatically remove the UI from the page when the content script is invalidated. + +See the [API Reference](/api/wxt/client/functions/createContentScriptUi) for the complete list of options. + +:::info TailwindCSS +`createContentScriptUi` supports TailwindCSS out of the box! When importing the styles, just import the main CSS file containing the `@tailwind` directives, and everything will just work :+1:. +::: + +When using a frontend framework for your UI, you'll need to unmount the app when the UI is removed. When defining the UI, return an app reference from the `mount` option and pass in a custom `onRemoved` option: + +:::code-group + +```ts [Vue] +import { createApp } from 'vue'; + +const ui = createContentScriptUi(ctx, { + // ... + mount(container) { + const app = createApp(App); + app.mount(container); + return app; + }, + onRemove(app) { + app.unmount(); + }, +}); +``` + +```tsx [React] +import ReactDOM from 'react-dom/client'; + +const ui = createContentScriptUi(ctx, { + // ... + mount(container) { + const root = ReactDOM.createRoot(container); + root.render(...); + return root; + }, + onRemove(root) { + root.unmount(); + }, +}); +``` + +```ts [Svelte] +import App from './App.svelte'; + +const ui = createContentScriptUi(ctx, { + // ... + mount(container) { + return new App({ target: container }); + }, + onRemove(app) { + app.$destry(); + }, +}); +``` + +```tsx [Solid] +import { render } from 'solid-js/web'; + +const ui = createContentScriptUi(ctx, { + // ... + mount(container) { + return render(() => ..., container); + }, + onRemove(unmount) { + unmount(); + }, +}); +``` + +::: + +:::warning +The `mount(container)` and `onRemove(app)` options passed into `createContentScriptUi` **_are different from_** the `ui.mount()` and `ui.remove()` functions available on the returned UI object. + +You don't need to pass anything into `ui.mount()` and `ui.remove()` because **_you already defined how and where the UI will be mounted_** in the options passed into `createContentScriptUi`. +::: + +## IFrame + +If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**. + +WXT provides a helper function, [`createContentScriptIframe`](/api/wxt/client/functions/createContentScriptUi), which simplifies setting up the IFrame. + +1. Create an HTML page that will be loaded into your IFrame + ```html + + + +
+ + +