Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"author": "Neek Sandhu <neek.sandhu@outlook.com>",
"scripts": {
"build": "tsup src/index.ts src/background.ts src/content-script.ts src/devtools.ts src/options.ts src/popup.ts src/window.ts --format esm,cjs --dts",
"build": "tsup src/index.ts src/background.ts src/content-script.ts src/devtools.ts src/options.ts src/popup.ts src/window.ts src/sidepanel.ts --format esm,cjs --dts",
"watch": "npm run build -- --watch",
"release": "bumpp --commit --push --tag && npm run build && npm publish"
},
Expand Down Expand Up @@ -55,6 +55,10 @@
"./window": {
"import": "./dist/window.js",
"require": "./dist/window.cjs"
},
"./sidepanel": {
"import": "./dist/sidepanel.js",
"require": "./dist/sidepanel.cjs"
}
},
"typesVersions": {
Expand All @@ -79,6 +83,9 @@
],
"window": [
"dist/window.d.ts"
],
"sidepanel": [
"dist/sidepanel.d.ts"
]
}
},
Expand Down
17 changes: 17 additions & 0 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,23 @@ browser.runtime.onConnect.addListener((incomingPort) => {
frameId: incomingPort.sender.frameId,
})

// For sidepanel, popup, options and devtools, we need the tab ID of the current tab
if (['sidepanel', 'popup', 'options', 'devtools'].includes(parseEndpoint(connArgs.endpointName).context)) {
browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {
if (tabs[0]) {
const activeTabId = tabs[0].id
// Update the endpoint name with the active tab ID for sidepanel
if (parseEndpoint(connArgs.endpointName).context === 'sidepanel' &&
parseEndpoint(connArgs.endpointName).tabId == null) {
connArgs.endpointName = formatEndpoint({
context: 'sidepanel',
tabId: activeTabId,
})
}
}
})
}

// literal tab id in case of content script, however tab id of inspected page in case of devtools context
const { tabId: linkedTabId, frameId: linkedFrameId } = parseEndpoint(
connArgs.endpointName,
Expand Down
4 changes: 2 additions & 2 deletions src/internal/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Endpoint, RuntimeContext } from '../types'

const ENDPOINT_RE = /^((?:background$)|devtools|popup|options|content-script|window)(?:@(\d+)(?:\.(\d+))?)?$/
const ENDPOINT_RE = /^((?:background$)|devtools|popup|options|content-script|window|sidepanel)(?:@(\d+)(?:\.(\d+))?)?$/

export const parseEndpoint = (endpoint: string): Endpoint => {
const [, context, tabId, frameId] = endpoint.match(ENDPOINT_RE) || []
Expand All @@ -13,7 +13,7 @@ export const parseEndpoint = (endpoint: string): Endpoint => {
}

export const formatEndpoint = ({ context, tabId, frameId }: Endpoint): string => {
if (['background', 'popup', 'options'].includes(context))
if (['background', 'popup', 'options', 'sidepanel'].includes(context))
return context

return `${context}@${tabId}${frameId ? `.${frameId}` : ''}`
Expand Down
2 changes: 1 addition & 1 deletion src/internal/is-internal-endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Endpoint, RuntimeContext } from '../types'

const internalEndpoints: RuntimeContext[] = ['background', 'devtools', 'content-script', 'options', 'popup']
const internalEndpoints: RuntimeContext[] = ['background', 'devtools', 'content-script', 'options', 'popup', 'sidepanel']

export const isInternalEndpoint = ({ context: ctx }: Endpoint): boolean => internalEndpoints.includes(ctx)
108 changes: 108 additions & 0 deletions src/sidepanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { createEndpointRuntime } from './internal/endpoint-runtime'
import { createStreamWirings } from './internal/stream'
import { createPersistentPort } from './internal/persistent-port'
import browser from 'webextension-polyfill'

// Chrome API types for sidepanel
declare global {
interface Chrome {
sidePanel?: {
setPanelBehavior: (options: { openPanelOnActionClick: boolean }) => void
setOptions: (options: { path?: string }) => void
onShown: {
addListener: (callback: () => void) => void
removeListener: (callback: () => void) => void
hasListener: (callback: () => void) => boolean
}
onHidden: {
addListener: (callback: () => void) => void
removeListener: (callback: () => void) => void
hasListener: (callback: () => void) => boolean
}
// V3 还支持指定页面的侧边栏配置
getOptions: (options: { tabId?: number }) => Promise<{ path?: string }>
}
}
var chrome: Chrome | undefined
}

const port = createPersistentPort('sidepanel')
const endpointRuntime = createEndpointRuntime(
'sidepanel',
message => port.postMessage(message),
)

port.onMessage(endpointRuntime.handleMessage)

/**
* Set up Chrome's sidepanel API for Manifest V3 extensions
*
* This function initializes the Chrome sidepanel API and configures its behavior.
* Use this in your sidepanel entry point to ensure the sidepanel works correctly.
*
* Example usage in your sidepanel script:
*
* ```ts
* import { setupSidepanel, sendMessage } from 'webext-bridge/sidepanel'
*
* // Initialize the sidepanel
* setupSidepanel({ defaultPath: 'sidepanel.html' })
*
* // Send a message to background
* sendMessage('get-data', { key: 'value' }, 'background')
* .then(response => console.log(response))
*
* // Listen for messages from other contexts
* onMessage('update-sidebar', (message) => {
* console.log(message.data)
* // Update sidebar UI
* })
* ```
*
* @param options Configuration options for the sidepanel
* @param options.defaultPath Default HTML path for the sidepanel
*/
export function setupSidepanel(options: { defaultPath?: string } = {}) {
if (typeof chrome !== 'undefined' && chrome.sidePanel) {
// Chrome specific sidepanel API
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })

if (options.defaultPath) {
chrome.sidePanel.setOptions({ path: options.defaultPath })
}
}
}

/**
* 注册侧边栏显示事件的回调函数
* @param callback 当侧边栏显示时要执行的回调函数
* @returns 用于移除事件监听器的函数
*/
export function onSidepanelShown(callback: () => void): () => void {
if (typeof chrome !== 'undefined' && chrome.sidePanel && chrome.sidePanel.onShown) {
chrome.sidePanel.onShown.addListener(callback)
return () => chrome.sidePanel.onShown.removeListener(callback)
}
return () => {}
}

/**
* 注册侧边栏隐藏事件的回调函数
* @param callback 当侧边栏隐藏时要执行的回调函数
* @returns 用于移除事件监听器的函数
*/
export function onSidepanelHidden(callback: () => void): () => void {
if (typeof chrome !== 'undefined' && chrome.sidePanel && chrome.sidePanel.onHidden) {
chrome.sidePanel.onHidden.addListener(callback)
return () => chrome.sidePanel.onHidden.removeListener(callback)
}
return () => {}
}

export function isSidepanelSupported(): boolean {
return !!chrome.sidePanel
}


export const { sendMessage, onMessage } = endpointRuntime
export const { openStream, onOpenStreamChannel } = createStreamWirings(endpointRuntime)
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type RuntimeContext =
| 'options'
| 'content-script'
| 'window'
| 'sidepanel'

export interface Endpoint {
context: RuntimeContext
Expand Down