diff --git a/biome.json b/biome.json index c652017..53b2fa4 100644 --- a/biome.json +++ b/biome.json @@ -25,7 +25,8 @@ "includes": [ "src/**/*.{ts,tsx,js,jsx}", "**/*.{json,jsonc,md,mdx,cjs,mjs}", - "!dist/**/*" + "!dist/**/*", + "!addon/**/*" ] }, "formatter": { diff --git a/docs/sidebar.md b/docs/sidebar.md index 6712134..397d7c2 100644 --- a/docs/sidebar.md +++ b/docs/sidebar.md @@ -6,16 +6,26 @@ Documentation: - [Opera Sidebar Action API](https://help.opera.com/en/extensions/sidebar-action-api/) - [Firefox Sidebar Action API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction) -A promise-based wrapper around Chrome's side panel (`chrome.sidePanel`, MV3) and Firefox/Opera `sidebarAction` APIs. Methods provide unified behavior to get/set options and behavior, open the panel, and, where supported, manage title and icon (Firefox & Opera) and badge (Opera). +A promise-based wrapper around Chromium's side panel (`chrome.sidePanel`, MV3) and Firefox/Opera `sidebarAction` APIs. Methods provide unified behavior to get/set options and behavior, open the panel, and, where supported, manage title and icon (Firefox & Opera) and badge (Opera). Support includes Chrome, Edge, Firefox, and Opera. + +## Classes + +### SidebarError + +Custom error class thrown when an API method is not supported or fails. ## Methods -- [getSidebarOptions(tabId?)](#getSidebarOptions) -- [getSidebarBehavior()](#getSidebarBehavior) +- [getSidebarOptions(tabId?)](#getSidebarOptions) [Chromium] +- [getSidebarBehavior()](#getSidebarBehavior) [Chromium] - [canOpenSidebar()](#canOpenSidebar) +- [canCloseSidebar()](#canCloseSidebar) - [openSidebar(options)](#openSidebar) -- [setSidebarOptions(options?)](#setSidebarOptions) -- [setSidebarBehavior(behavior?)](#setSidebarBehavior) +- [closeSidebar(options)](#closeSidebar) +- [setSidebarOptions(options?)](#setSidebarOptions) [Chromium] +- [setSidebarBehavior(behavior?)](#setSidebarBehavior) [Chromium] +- [isOpenSidebar(windowId?)](#isOpenSidebar) +- [toggleSidebar()](#toggleSidebar) [Firefox] - [setSidebarPath(path, tabId?)](#setSidebarPath) - [getSidebarPath(tabId?)](#getSidebarPath) - [setSidebarTitle(title, tabId?)](#setSidebarTitle) [Firefox, Opera] @@ -33,23 +43,23 @@ A promise-based wrapper around Chrome's side panel (`chrome.sidePanel`, MV3) and -### getSidebarOptions +### getSidebarOptions [Chromium] ``` getSidebarOptions(tabId?: number): Promise ``` -Retrieves the side panel options (e.g., `path`) for the specified tab. Throws if the Side Panel API isn't supported. [MV3] +Retrieves the side panel options (e.g., `path`) for the specified tab. Throws if the Side Panel API isn't supported (requires Chromium-based browsers like Chrome, Edge, or Opera MV3). [MV3] -### getSidebarBehavior +### getSidebarBehavior [Chromium] ``` getSidebarBehavior(): Promise ``` -Gets the current side panel behavior settings. Throws if unsupported. [MV3] +Gets the current side panel behavior settings. Throws if unsupported (requires Chromium-based browsers like Chrome, Edge, or Opera MV3). [MV3] @@ -59,7 +69,17 @@ Gets the current side panel behavior settings. Throws if unsupported. [MV3] canOpenSidebar(): boolean ``` -Returns `true` if `chrome.sidePanel` (MV3) is available, or if `browser.sidebarAction.open` is available (Firefox). +Returns `true` if `chrome.sidePanel` (Chromium MV3) is available, or if `sidebarAction.open` is available (Firefox/Opera). + + + +### canCloseSidebar + +``` +canCloseSidebar(): boolean +``` + +Returns `true` if `chrome.sidePanel` (Chromium MV3) is available, or if `sidebarAction.close` is available (Firefox/Opera). @@ -69,27 +89,37 @@ Returns `true` if `chrome.sidePanel` (MV3) is available, or if `browser.sidebarA openSidebar(options: chrome.sidePanel.OpenOptions): Promise ``` -Opens the side panel with the given options in Chrome (MV3). Falls back to `browser.sidebarAction.open()` in Firefox. Logs a warning and resolves as a no-op if unsupported. +Opens the side panel with the given options in Chromium-based browsers (MV3). Falls back to `sidebarAction.open()` in Firefox/Opera. Throws if unsupported. + + + +### closeSidebar + +``` +closeSidebar(options: chrome.sidePanel.CloseOptions): Promise +``` + +Closes the side panel with the given options in Chromium-based browsers (MV3). Falls back to `sidebarAction.close()` in Firefox/Opera. Throws if unsupported. -### setSidebarOptions +### setSidebarOptions [Chromium] ``` setSidebarOptions(options?: chrome.sidePanel.PanelOptions): Promise ``` -Sets side panel options (e.g., `path`) in Chrome (MV3). Logs a warning and resolves as a no-op if unsupported. [MV3] +Sets side panel options (e.g., `path`) in Chromium-based browsers (MV3). Throws if unsupported. [MV3] -### setSidebarBehavior +### setSidebarBehavior [Chromium] ``` setSidebarBehavior(behavior?: chrome.sidePanel.PanelBehavior): Promise ``` -Updates default panel behavior in Chrome (MV3). Logs a warning and resolves as a no-op if unsupported. [MV3] +Updates default panel behavior in Chromium-based browsers (MV3). Throws if unsupported. [MV3] @@ -99,7 +129,7 @@ Updates default panel behavior in Chrome (MV3). Logs a warning and resolves as a setSidebarPath(path: string, tabId?: number): Promise ``` -Sets the sidebar path in Chrome via `setOptions` (MV3) or via `sidebarAction.setPanel()` in Firefox/Opera. Throws if unsupported. +Sets the sidebar path in Chromium-based browsers via `setOptions` (MV3) or via `sidebarAction.setPanel()` in Firefox/Opera. Throws if unsupported. @@ -109,7 +139,27 @@ Sets the sidebar path in Chrome via `setOptions` (MV3) or via `sidebarAction.set getSidebarPath(tabId?: number): Promise ``` -Retrieves the sidebar path from Chrome (MV3) or parses from `sidebarAction.getPanel()` in Firefox/Opera. Throws if unsupported. +Retrieves the sidebar path from Chromium-based browsers (MV3) or parses from `sidebarAction.getPanel()` in Firefox/Opera. Throws if unsupported. + + + +### isOpenSidebar + +``` +isOpenSidebar(windowId?: number): Promise +``` + +Checks if the sidebar is open for the given window in Chromium-based browsers (MV3) using `getContexts` and in Firefox/Opera using `sidebarAction.isOpen()`. Throws if unsupported. + + + +### toggleSidebar [Firefox] + +``` +toggleSidebar(): Promise +``` + +Toggles the sidebar in Firefox. Throws if unsupported. @@ -119,7 +169,7 @@ Retrieves the sidebar path from Chrome (MV3) or parses from `sidebarAction.getPa setSidebarTitle(title: string | number, tabId?: number): Promise ``` -Sets the sidebar title via `sidebarAction.setTitle()` (Firefox/Opera). Logs a warning if unsupported. +Sets the sidebar title via `sidebarAction.setTitle()` (Firefox/Opera). Throws if unsupported. @@ -129,7 +179,7 @@ Sets the sidebar title via `sidebarAction.setTitle()` (Firefox/Opera). Logs a wa setSidebarIcon(details: opr.sidebarAction.IconDetails): Promise ``` -Sets the sidebar icon via `sidebarAction.setIcon()` (Firefox/Opera). Logs a warning if unsupported. +Sets the sidebar icon via `sidebarAction.setIcon()` (Firefox/Opera). Throws if unsupported. > Known issue (Opera): The `opr.sidebarAction.setIcon` API is currently broken and may fail with "Access to extension API denied". > See: https://forums.opera.com/topic/75680/opr-sidebaraction-seticon-api-is-broken-access-to-extension-api-denied @@ -142,7 +192,7 @@ Sets the sidebar icon via `sidebarAction.setIcon()` (Firefox/Opera). Logs a warn setSidebarBadgeText(text: string | number, tabId?: number): Promise ``` -Sets the sidebar badge text via `opr.sidebarAction.setBadgeText()` (Opera only). Logs a warning if unsupported. +Sets the sidebar badge text via `opr.sidebarAction.setBadgeText()` (Opera only). Throws if unsupported. @@ -162,7 +212,7 @@ Clears the sidebar badge text (equivalent to setting an empty string) via `opr.s setSidebarBadgeTextColor(color: string | [number, number, number, number], tabId?: number): Promise ``` -Sets the sidebar badge text color via `opr.sidebarAction.setBadgeTextColor()` (Opera only). Logs a warning if unsupported. +Sets the sidebar badge text color via `opr.sidebarAction.setBadgeTextColor()` (Opera only). Throws if unsupported. @@ -172,7 +222,7 @@ Sets the sidebar badge text color via `opr.sidebarAction.setBadgeTextColor()` (O setSidebarBadgeBgColor(color: string | [number, number, number, number], tabId?: number): Promise ``` -Sets the sidebar badge background color via `opr.sidebarAction.setBadgeBackgroundColor()` (Opera only). Logs a warning if unsupported. +Sets the sidebar badge background color via `opr.sidebarAction.setBadgeBackgroundColor()` (Opera only). Throws if unsupported. diff --git a/package-lock.json b/package-lock.json index efc9d48..5bb1226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@commitlint/cli": "^20.0.0", "@commitlint/config-conventional": "^20.0.0", "@release-it/conventional-changelog": "^10.0.1", - "@types/chrome": "^0.1.12", + "@types/chrome": "^0.1.36", "@types/jest": "^30.0.0", "husky": "^9.1.7", "jest": "^30.1.3", diff --git a/package.json b/package.json index 8317bac..32dc0d6 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@commitlint/cli": "^20.0.0", "@commitlint/config-conventional": "^20.0.0", "@release-it/conventional-changelog": "^10.0.1", - "@types/chrome": "^0.1.12", + "@types/chrome": "^0.1.36", "@types/jest": "^30.0.0", "husky": "^9.1.7", "jest": "^30.1.3", diff --git a/src/sidebar.ts b/src/sidebar.ts index b5dbfaa..da08cc0 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -1,4 +1,5 @@ import {browser} from "./browser"; +import {getContexts} from "./runtime"; import {callWithPromise} from "./utils"; import type {FirefoxSidebarAction, OperaSidebarAction, SidebarAction} from "./types"; @@ -6,8 +7,10 @@ type Color = string | ColorArray; type ColorArray = chrome.extensionTypes.ColorArray; type OpenOptions = chrome.sidePanel.OpenOptions; +type CloseOptions = chrome.sidePanel.CloseOptions; type PanelOptions = chrome.sidePanel.PanelOptions; type PanelBehavior = chrome.sidePanel.PanelBehavior; +type ContextFilter = chrome.runtime.ContextFilter; type IconDetails = opr.sidebarAction.IconDetails; // Available in Firefox and Opera @@ -19,6 +22,8 @@ const isAvailableOperaSidebar = (): boolean => globalThis?.opr?.sidebarAction != // Chromium standard const sidePanel = (): typeof chrome.sidePanel | undefined => browser().sidePanel; +export class SidebarError extends Error {} + // Methods export const getSidebarOptions = (tabId?: number): Promise => callWithPromise(cb => { @@ -56,8 +61,22 @@ export const canOpenSidebar = (): boolean => { return false; }; +export const canCloseSidebar = (): boolean => { + if (sidePanel()) { + return true; + } + + const sb = (sidebarAction() as FirefoxSidebarAction) || undefined; + + if (sb) { + return !!sb.close; + } + + return false; +}; + export const openSidebar = (options: OpenOptions): Promise => - callWithPromise(cb => { + callWithPromise(async cb => { const sp = sidePanel(); if (sp) { @@ -67,12 +86,39 @@ export const openSidebar = (options: OpenOptions): Promise => const sb = sidebarAction() as FirefoxSidebarAction | undefined; if (sb?.open) { - return sb.open(); + const result = sb.open(); + + if (result instanceof Promise) { + await result; + } + + return cb(); + } + + throw new SidebarError("The sidebarAction.open API is not supported in this browser"); + }); + +export const closeSidebar = (options: CloseOptions): Promise => + callWithPromise(async cb => { + const sp = sidePanel(); + + if (sp) { + return sp.close(options, cb); } - console.warn("The sidebar open API is not supported in this browser"); + const sb = sidebarAction() as FirefoxSidebarAction | undefined; - cb(); + if (sb?.close) { + const result = sb.close(); + + if (result instanceof Promise) { + await result; + } + + return cb(); + } + + throw new SidebarError("The sidebarAction.close API is not supported in this browser"); }); export const setSidebarOptions = (options?: PanelOptions): Promise => @@ -80,9 +126,7 @@ export const setSidebarOptions = (options?: PanelOptions): Promise => const sp = sidePanel(); if (!sp) { - console.warn("The chrome.sidePanel.setOptions API is not supported for this browser"); - - return cb(); + throw new SidebarError("The chrome.sidePanel.setOptions API is not supported for this browser"); } sp.setOptions(options || {}, cb); @@ -93,12 +137,53 @@ export const setSidebarBehavior = (behavior?: PanelBehavior): Promise => const sp = sidePanel(); if (!sp) { - console.warn("The chrome.sidePanel.setPanelBehavior API is not supported in this browser"); + throw new SidebarError("The chrome.sidePanel.setPanelBehavior API is not supported in this browser"); + } + + sp.setPanelBehavior(behavior || {}, cb); + }); + +export const isOpenSidebar = async (windowId?: number): Promise => { + if (sidePanel()) { + const filter: ContextFilter = {contextTypes: ["SIDE_PANEL"]}; + + if (windowId) filter.windowIds = [windowId]; + + return (await getContexts(filter)).length !== 0; + } + + return callWithPromise(async cb => { + const sb = sidebarAction() as FirefoxSidebarAction | undefined; + + if (sb?.isOpen) { + const result = sb.isOpen({windowId}); + + if (result instanceof Promise) { + return cb(await result); + } else { + return cb(result); + } + } + + throw new SidebarError("The sidebarAction.isOpen API is not supported in this browser"); + }); +}; + +export const toggleSidebar = (): Promise => + callWithPromise(async cb => { + const sb = sidebarAction() as FirefoxSidebarAction | undefined; + + if (sb?.toggle) { + const result = sb.toggle(); + + if (result instanceof Promise) { + await result; + } return cb(); } - sp.setPanelBehavior(behavior || {}, cb); + throw new SidebarError("The sidebarAction.toggle API is not supported in this browser"); }); // Customs methods @@ -153,19 +238,16 @@ export const setSidebarTitle = (title: string | number, tabId?: number): Promise callWithPromise(async cb => { const sb = sidebarAction(); - if (!sb) { - console.warn("The sidebarAction.setTitle API is supported only in Opera or Firefox"); + if (sb?.setTitle) { + const result = sb.setTitle({tabId, title: title.toString()}); + if (result instanceof Promise) { + await result; + } return cb(); } - const result = sb.setTitle({tabId, title: title.toString()}); - - if (result instanceof Promise) { - await result; - } - - cb(); + throw new SidebarError("The sidebarAction.setTitle API is supported only in Opera or Firefox"); }); export const setSidebarBadgeText = (text: string | number, tabId?: number): Promise => @@ -178,9 +260,7 @@ export const setSidebarBadgeText = (text: string | number, tabId?: number): Prom return cb(); } - console.warn("The opr.sidebarAction.setBadgeText API is supported only in Opera"); - - cb(); + throw new SidebarError("The sidebarAction.setBadgeText API is supported only in Opera"); }); export const clearSidebarBadgeText = (tabId?: number): Promise => setSidebarBadgeText("", tabId); @@ -211,9 +291,7 @@ export const setSidebarIcon = (details: IconDetails): Promise => return cb(); } - console.warn("The sidebarAction.setIcon API is supported only in Opera or Firefox"); - - cb(); + throw new SidebarError("The sidebarAction.setIcon API is supported only in Opera or Firefox"); }); export const setSidebarBadgeTextColor = (color: Color, tabId?: number): Promise => @@ -226,9 +304,7 @@ export const setSidebarBadgeTextColor = (color: Color, tabId?: number): Promise< return cb(); } - console.warn("The opr.sidebarAction.setBadgeTextColor API is supported only in Opera"); - - cb(); + throw new SidebarError("The sidebarAction.setBadgeTextColor API is supported only in Opera"); }); export const setSidebarBadgeBgColor = (color: Color, tabId?: number): Promise => @@ -241,9 +317,7 @@ export const setSidebarBadgeBgColor = (color: Color, tabId?: number): Promise => @@ -258,7 +332,7 @@ export const getSidebarTitle = (tabId?: number): Promise => return (sb as any).getTitle({tabId}); } - throw new Error("The sidebarAction.getTitle API not available"); + throw new SidebarError("The sidebarAction.getTitle API not available"); }); export const getSidebarBadgeText = (tabId?: number): Promise => @@ -269,7 +343,7 @@ export const getSidebarBadgeText = (tabId?: number): Promise => return sb.getBadgeText({tabId}, cb); } - throw new Error("The opr.sidebarAction.getBadgeText API is supported only in Opera"); + throw new SidebarError("The sidebarAction.getBadgeText API is supported only in Opera"); }); export const getSidebarBadgeTextColor = (tabId?: number): Promise => @@ -280,7 +354,7 @@ export const getSidebarBadgeTextColor = (tabId?: number): Promise => return sb.getBadgeTextColor({tabId}, cb); } - throw new Error("The opr.sidebarAction.getBadgeTextColor API is supported only in Opera"); + throw new SidebarError("The sidebarAction.getBadgeTextColor API is supported only in Opera"); }); export const getSidebarBadgeBgColor = (tabId?: number): Promise => @@ -291,5 +365,5 @@ export const getSidebarBadgeBgColor = (tabId?: number): Promise => return sb.getBadgeBackgroundColor({tabId}, cb); } - throw new Error("The opr.sidebarAction.getBadgeBackgroundColor API is supported only in Opera"); + throw new SidebarError("The sidebarAction.getBadgeBackgroundColor API is supported only in Opera"); });