1"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[3654],{8530:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var i=s(4848),t=s(8453);const r={},o=void 0,a={id:"chrome-extensions/API",title:"API",description:"Action",source:"@site/docs/chrome-extensions/02-API.md",sourceDirName:"chrome-extensions",slug:"/chrome-extensions/API",permalink:"/docs/chrome-extensions/API",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:2,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"intro",permalink:"/docs/chrome-extensions/intro"},next:{title:"Plasmo",permalink:"/docs/chrome-extensions/plasmo"}},c={},l=[{value:"Action",id:"action",level:2},{value:"Alarms",id:"alarms",level:2},{value:"Creating alarms",id:"creating-alarms",level:3},{value:"Extra API",id:"extra-api",level:3},{value:"Example",id:"example",level:3},{value:"Bookmarks",id:"bookmarks",level:2},{value:"Commands",id:"commands",level:2},{value:"Context Menus",id:"context-menus",level:2},{value:"Class",id:"class",level:3},{value:"Example",id:"example-1",level:3},{value:"declarativeContent",id:"declarativecontent",level:2},{value:"declarativeNetRequest",id:"declarativenetrequest",level:2},{value:"Static blocking",id:"static-blocking",level:3},{value:"Static blocking",id:"static-blocking-1",level:3},{value:"Dynamic blocking",id:"dynamic-blocking",level:3},{value:"Extension",id:"extension",level:2},{value:"Identity",id:"identity",level:2},{value:"Idle",id:"idle",level:2},{value:"Management",id:"management",level:2},{value:"Notifications",id:"notifications",level:2},{value:"Showing a basic notification",id:"showing-a-basic-notification",level:3},{value:"Events",id:"events",level:3},{value:"Class",id:"class-1",level:3},{value:"Permissions",id:"permissions",level:2},{value:"Docs",id:"docs",level:3},{value:"Class",id:"class-2",level:3},{value:"ReadingList",id:"readinglist",level:2},{value:"Types",id:"types",level:3},{value:"API",id:"api",level:3},{value:"Runtime",id:"runtime",level:2},{value:"Opening a uninstall page",id:"opening-a-uninstall-page",level:3},{value:"API",id:"api-1",level:3},{value:"scripting",id:"scripting",level:2},{value:"Injecting function example",id:"injecting-function-example",level:3},{value:"Full API",id:"full-api",level:3},{value:"Class",id:"class-3",level:3},{value:"Tabs",id:"tabs",level:2},{value:"Permissions",id:"permissions-1",level:3},{value:"API",id:"api-2",level:3},{value:"Use cases",id:"use-cases",level:3},{value:"Creating a tab",id:"creating-a-tab",level:4},{value:"Querying tabs",id:"querying-tabs",level:4},{value:"Screenshots",id:"screenshots",level:4},{value:"Dealing with page zoom",id:"dealing-with-page-zoom",level:4},{value:"Grouping tabs",id:"grouping-tabs",level:4},{value:"Listening for tab changes",id:"listening-for-tab-changes",level:4},{value:"TabGroups",id:"tabgroups",level:2},{value:"userScripts",id:"userscripts",level:2},{value:"Setup",id:"setup",level:3},{value:"API",id:"api-3",level:3},{value:"The major caveat",id:"the-major-caveat",level:3},{value:"webNavigation",id:"webnavigation",level:2},{value:"Windows",id:"windows",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h2,{id:"action",children:"Action"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.action.onClicked.addListener(tab => {})"})," : this event fires when the user clicks on the action icon. This event will not fire if a popup is defined."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"alarms",children:"Alarms"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"alarms"})," API can be accessed through granting the ",(0,i.jsx)(n.code,{children:'"alarms"'})," permission."]}),"\n",(0,i.jsx)(n.p,{children:"Alarms generally persist until an extension is updated. However, this is not guaranteed, and alarms may be cleared when the browser is restarted. Consequently, consider setting a value in storage when an alarm is created, and then ensure it exists each time your service worker starts up. For example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:'const STORAGE_KEY = "user-preference-alarm-enabled";\n\nasync function checkAlarmState() {\n const { alarmEnabled } = await chrome.storage.get(STORAGE_KEY);\n\n if (alarmEnabled) {\n const alarm = await chrome.alarms.get("my-alarm");\n\n if (!alarm) {\n await chrome.alarms.create({ periodInMinutes: 1 });\n }\n }\n}\n\ncheckAlarmState();\n'})}),"\n",(0,i.jsx)(n.h3,{id:"creating-alarms",children:"Creating alarms"}),"\n",(0,i.jsx)(n.p,{children:"This is the basic way to create and handle alarms:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"// 1. create the alarm\n chrome.alarms.create('demo-default-alarm', {\n delayInMinutes: 1,\n periodInMinutes: 1,\n when: new Date(\"2024-08-01\")\n });\n \n// 2. listen for the alarm\nchrome.alarms.onAlarm.addListener(alarm => {\n\t// do something here\n})\n"})}),"\n",(0,i.jsxs)(n.p,{children:["An ",(0,i.jsx)(n.code,{children:"Alarm"})," instance looks like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"interface Alarm {\n\tname: string // alarm name\n\tperiodInMinutes: number // alarm period\n\tscheduledTime: number // time at which the alarm was scheduled to fire\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"extra-api",children:"Extra API"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.alarms.create(name, options)"})," : creates an alarm with the specified name and options. Here are the keys of the options object you can pass:","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"delayInMinutes: number"})," : the delay in minutes before alarm begins"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"periodInMinutes: number"})," : how long an alarm should wait before repeating. This has a minimum value of 30 seconds, or ",(0,i.jsx)(n.code,{children:"0.5"})," in minutes."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"when: Date"})," : when the alarm should trigger."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.alarms.onAlarm.addListener(alarm => ...)"}
1)," : whenever an alarm fires, the callback you provide is triggered."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"And here are the asynchronous methods:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.alarms.clear(name)"})," : clears the alarm with the specified name"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.alarms.clearAll()"})," : clears all alarms"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.alarms.get(name)"})," : returns an ",(0,i.jsx)(n.code,{children:"Alarm"})," instance representing the alarm with the specified name."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.alarms.getAll()"})," : returns an array of all the ",(0,i.jsx)(n.code,{children:"Alarm"})," instances in your app"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"example",children:"Example"}),"\n",(0,i.jsx)(n.p,{children:"Here is a simple alarm class and a suitable example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"export class ChromeAlarm {\n private alarmCB?: (alarm: chrome.alarms.Alarm) => void;\n constructor(private alarmName: string) {}\n\n async createAlarm(alarmInfo: chrome.alarms.AlarmCreateInfo) {\n await chrome.alarms.create(this.alarmName, alarmInfo);\n }\n\n async upsertAlarm(alarmInfo: chrome.alarms.AlarmCreateInfo) {\n const alarm = await this.getAlarm();\n if (!alarm) {\n await this.createAlarm(alarmInfo);\n return null;\n }\n return alarm;\n }\n\n async getAlarm() {\n const alarm = await chrome.alarms.get(this.alarmName);\n if (!alarm) {\n return null;\n }\n return alarm;\n }\n\n onTriggered(callback: () => void) {\n const alarmCB = (alarm: chrome.alarms.Alarm) => {\n if (alarm.name === this.alarmName) {\n callback();\n }\n };\n this.alarmCB = alarmCB;\n chrome.alarms.onAlarm.addListener(alarmCB);\n }\n\n /**\n *\n * @returns wasCleared: True if the alarm was successfully cleared, false otherwise.\n */\n async clearAlarm() {\n this.alarmCB && chrome.alarms.onAlarm.removeListener(this.alarmCB);\n return await chrome.alarms.clear(this.alarmName);\n }\n}\n"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'import { ChromeAlarm } from "../utils/api/alarms";\nimport NotificationModel from "../utils/api/notifications";\nimport { Runtime } from "../utils/api/runtime";\n\nconst installAlarm = new ChromeAlarm("installAlarm");\n\nRuntime.onInstall({\n // runs first time you download the extension\n installCb: async () => {\n console.log("Extension installed");\n },\n // runs every time you update the extension or refresh it\n updateCb: async () => {\n console.log("Extension updated");\n const curAlarm = await installAlarm.upsertAlarm({\n when: Date.now() + 1000,\n periodInMinutes: 0.5,\n });\n },\n});\n\ninstallAlarm.onTriggered(() => {\n console.log("Alarm triggered");\n NotificationModel.showBasicNotification({\n title: "Extension Updated",\n message: "Extension has been updated successfully",\n iconPath: "public/icon.png",\n });\n});\n'})}),"\n",(0,i.jsx)(n.h2,{id:"bookmarks",children:"Bookmarks"}),"\n",(0,i.jsx)(n.h2,{id:"commands",children:"Commands"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"commands"})," key in the manifest allows you to register global keyboard shortcuts for your extension. Keyboard shortcuts only work when the browser is in focus."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Users can remap the command by going to ",(0,i.jsx)(n.a,{href:"chrome://extensions/shortcuts",children:"chrome extension shortcuts"})]})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:' "commands": {\n "run-foo": {\n "suggested_key": {\n "default": "Ctrl+Shift+Y",\n "mac": "Command+Shift+Y"\n },\n "description": "Run \\"foo\\" on the current page."\n },\n "_execute_action": {\n "suggested_key": {\n "windows": "Ctrl+Shift+Y",\n "mac": "Command+Shift+Y",\n "chromeos": "Ctrl+Shift+U",\n "linux": "Ctrl+Shift+J"\n }\n }\n },\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:'"_execute_action"'})," command is special because it simulates clicking the action of the extension."]}),"\n",(0,i.jsx)(n.p,{children:"You can then listen for the keyboard shortcut like so:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"\n// if normal keyboard shortcut is hit\nchrome.commands.onCommand.addListener((command) => {\n console.log(`Command: ${command}`);\n});\n\n// if _execute_action shortcut is hit\nchrome.action.onClicked.addListener((tab) => {\n \n});\n"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'export default class Commands {\n constructor(private commands: Record<string, () => void>) {}\n\n addListeners() {\n chrome.commands.onCommand.addListener((command) => {\n if (this.commands[command]) {\n this.commands[command]();\n }\n });\n }\n\n static onExecuteAction(cb: () => void) {\n chrome.action.onClicked.addListener(cb);\n }\n\n static async getAllCommands() {\n return await chrome.commands.getAll();\n }\n\n static async getActiveCommands() {\n const commands = await Commands.getAllCommands();\n return commands.filter((command) => command.shortcut !== "");\n }\n}\n'})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.commands.getAll()"})," : async method that returns the list of all the command names as a ",(0,i.jsx)(n.code,{children:"Command"})," instance."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"Command"})," instance has these properties:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"command.name"})," : the name"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"command.description"})," : the description"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"command.shortcut"})," : the keyboard shortcut, and blank if unassigned."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"context-menus",children:"Context Menus"}),"\n",(0,i.jsxs)(n.p,{children:["You can create context menu items easily with the ",(0,i.jsx)(n.code,{children:"chrome.contextMenus"})," API, which needs the ",(0,i.jsx)(n.code,{children:'"contextMenus"'})," permission in the manifest."]}),"\n",(0,i.jsxs)(n.p,{children:["You should create context menus at the beginning of the application, on the ",(0,i.jsx)(n.code,{children:"runtime.onInstalled"})," event."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.contextMenus.create(options)"})," : creates a context menu"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.contextMenus.remove(menuId)"})," : deletes the context menu with the specified menu id"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.contextMenus.removeAll()"})," : removes all context menus"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"You can then listen for a context menu click like so:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"chrome.contextMenus.onClicked.addListener(menu => {\n\t// do an action based on menu id\n})\n"})}),"\n",(0,i.jsx)(n.h3,{id:"class",children:"Class"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// requires the contextMenus API permission\n\nexport default class ContextMenu {\n static LIMIT = chrome.contextMenus.ACTION_MENU_TOP_LEVEL_LIMIT;\n static menuCount = 0;\n private cb?: (info: chrome.contextMenus.OnClickData) => void;\n constructor(public menuId: string) {}\n\n create(args: chrome.contextMenus.CreateProperties) {\n if (ContextMenu.menuCount >= ContextMenu.LIMIT) {\n throw new Error(\n `Exceeds the maximum number of top level menu items, which is ${ContextMenu.LIMIT}`\n );\n }\n chrome.contextMenus.create({\n ...args,\n id: this.menuId,\n });\n ContextMenu.menuCount++;\n }\n\n onClicked(callback: (info: chrome.contextMenus.OnClickData) => void) {\n const cb = (info: chrome.contextMenus.OnClickData) =>
1 {\n if (info.menuItemId === this.menuId) {\n callback(info);\n }\n };\n this.cb = cb;\n chrome.contextMenus.onClicked.addListener(this.cb);\n }\n\n update(args: chrome.contextMenus.UpdateProperties) {\n chrome.contextMenus.update(this.menuId, args);\n }\n\n remove() {\n chrome.contextMenus.remove(this.menuId);\n if (this.cb) {\n chrome.contextMenus.onClicked.removeListener(this.cb);\n }\n ContextMenu.menuCount--;\n }\n\n static removeAll() {\n chrome.contextMenus.removeAll();\n ContextMenu.menuCount = 0;\n }\n}\n\n"})}),"\n",(0,i.jsx)(n.h3,{id:"example-1",children:"Example"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'import ContextMenu from "../utils/api/contextMenu";\nimport NotificationModel from "../utils/api/notifications";\nimport { Runtime } from "../utils/api/runtime";\n\nconst menu = new ContextMenu("test");\nconst menu2 = new ContextMenu("test2");\n\nRuntime.onInstall({\n // runs first time you download the extension\n installCb: async () => {\n console.log("Extension installed");\n },\n // runs every time you update the extension or refresh it\n updateCb: async () => {\n console.log("Extension updated");\n menu.create({\n title: "Test",\n contexts: ["all"],\n });\n menu2.create({\n title: "Test 2",\n contexts: ["all"],\n });\n },\n});\n\nchrome.runtime.onSuspend.addListener(() => {\n menu.remove();\n});\n\nmenu.onClicked((info) => {\n NotificationModel.showBasicNotification({\n title: "Test",\n message: `on ${info.pageUrl}`,\n iconPath: "public/icon.png",\n });\n});\n\nmenu2.onClicked((info) => {\n NotificationModel.showBasicNotification({\n title: "Test 2",\n message: `on ${info.pageUrl}`,\n iconPath: "public/icon.png",\n });\n});\n'})}),"\n",(0,i.jsx)(n.h2,{id:"declarativecontent",children:"declarativeContent"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"declarativeContent"})," api, enabled with the ",(0,i.jsx)(n.code,{children:"declarativeContent"})," manifest permission allows you to automatically enable/disable the extension action."]}),"\n",(0,i.jsx)(n.p,{children:"This is the basic use:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// 1. create a rule\nlet rule2 = {\n conditions: [\n new chrome.declarativeContent.PageStateMatcher({\n pageUrl: { hostSuffix: '.google.com', schemes: ['https'] },\n css: [\"input[type='password']\"]\n }),\n new chrome.declarativeContent.PageStateMatcher({\n css: [\"video\"]\n })\n ],\n actions: [ new chrome.declarativeContent.ShowAction() ]\n};\n\n// 2. register the rules\nchrome.declarativeContent.onPageChanged.removeRules(undefined, function () {\n chrome.declarativeContent.onPageChanged.addRules([rule2]);\n});\n"})}),"\n",(0,i.jsx)(n.p,{children:"Here is the class:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"export class DeclarativeContent {\n public readonly rules: chrome.events.Rule[] = [];\n\n addRule(\n pageMatchers: chrome.declarativeContent.PageStateMatcher[],\n options: {\n showAction?: boolean;\n } = {}\n ) {\n const rule: chrome.events.Rule = {\n conditions: pageMatchers,\n actions: [\n options.showAction && new chrome.declarativeContent.ShowAction(),\n ].filter((thing) => Boolean(thing)),\n };\n this.rules.push(rule);\n }\n\n registerRules() {\n const rules = this.rules;\n chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {\n chrome.declarativeContent.onPageChanged.addRules(rules);\n });\n }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"declarativenetrequest",children:"declarativeNetRequest"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"declarativeNetRequest"})," api requires the ",(0,i.jsx)(n.code,{children:"declarativeNetRequest"})," permission and can automatically block up to 300,000 web addresses based on their URL and the type of resource they are."]}),"\n",(0,i.jsx)(n.p,{children:"You can block either dynamically or statically."}),"\n",(0,i.jsx)(n.h3,{id:"static-blocking",children:"Static blocking"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Add the ",(0,i.jsx)(n.code,{children:'"declarativeNetRequest"'})," permission to the manifest"]}),"\n",(0,i.jsxs)(n.li,{children:["Add the ",(0,i.jsx)(n.code,{children:'"declarativeNetRequest"'})," in the key and point to a JSON ruleset you want to enable"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'"declarative_net_request": {\n"rule_resources": [\n {\n\t"id": "blocklist",\n\t"enabled": true,\n\t"path": "blocklist.json"\n }\n]\n},\n'})}),"\n",(0,i.jsxs)(n.ol,{start:"3",children:["\n",(0,i.jsx)(n.li,{children:"Write the JSON rules as follows. Each object must have a unique id starting at 1."}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'[\n {\n "id": 1,\n "priority": 1,\n "action": { "type": "block" },\n "condition": { "urlFilter": "*://*doubleclick.net/*" }\n },\n {\n "id": 2,\n "priority": 1,\n "action": { "type": "block" },\n "condition": { "urlFilter": "*://googleadservices.com/*" }\n },\n {\n "id": 3,\n "priority": 1,\n "action": { "type": "block" },\n "condition": { "urlFilter": "*://googlesyndication.com/*" }\n }\n]\n'})}),"\n",(0,i.jsx)(n.h3,{id:"static-blocking-1",children:"Static blocking"}),"\n",(0,i.jsx)(n.h3,{id:"dynamic-blocking",children:"Dynamic blocking"}),"\n",(0,i.jsx)(n.h2,{id:"extension",children:"Extension"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:'"extension"'})," permission and ",(0,i.jsx)(n.code,{children:"extension"})," API provides info about the extension itself. Here are some async methods to use:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.extension.isAllowedIncognitoAccess()"})," : returns whether or not the extension can run in incognito mode."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.extension.isAllowedFileSchemeAccess()"})," : returns whether or not the extension can access ",(0,i.jsx)(n.code,{children:"file"})," URI protocol files."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"identity",children:"Identity"}),"\n",(0,i.jsx)(n.p,{children:"Here are the async methods on the identity API:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.identity.getProfileUserInfo({});"})," : returns an object with a ",(0,i.jsx)(n.code,{children:".id"})," and a ",(0,i.jsx)(n.code,{children:".email"})," property of the currently logged in google account. Needs the ",(0,i.jsx)(n.code,{children:'"identity.email"'})," permission"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.identity.getAuthToken(options)"})," : returns a token object, and takes in some options.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"interactive"})," : whether or not the token acquiring process is done interactively."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"scopes"})," : a string list of OAuth scopes to request."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Here are the classes:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'/**\n * requires permissions: "identity.email" in manifest.json\n */\nexport class IdentityEmail {\n static async getProfile() {\n return await chrome.identity.getProfileUserInfo({});\n }\n}\n\n/**\n * requires permissions: "identity" in manifest.json\n */\nexport class Identity {\n static async getBasicAuthToken() {\n return await chrome.identity.getAuthToken({ interactive: true });\n }\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"idle",children:"Idle"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"idle"})," API requires the ",(0,i.jsx)(n.code,{children:'"idle"'})," permission and is used like so:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'type IdleStateCallback = (newState: "active" | "idle" | "locked") => void;\n\nexport default class Idle {\n /**\n * triggered when the system changes idle state\n */\n static onStateChanged(cb: IdleStateCallback) {\n chrome.idle.onStateChanged.addListener(cb);\n }\n\n /**\n * Returns the idle state of the system.\n * @param intervalSeconds must be greater than 15\n * @param cb\n */\n static async queryState(intervalSeconds: number) {\n await chrome.idle.queryState(Math.max(intervalSeconds, 15));\n }\n}\n'})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.idle.onStateChanged.addListener(state => {})"}),' : triggered at least every 60 seconds, and only when idle state changes. The state can be either "active", "idle", or "locked"']}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.idle.queryState(numSeconds)"})," : returns the current state of the system. It's asynchronous."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"management",children:"Management"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"chrome.management"})," API requires the ",(0,i.jsx)(n.code,{children:'"management"'})," permission and is used to manage extensions, including other extensions. A simple use case is listening for when an extension is uninstalled, and then creating a tab to beg the user to reinstall."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"interface ExtensionListeners {\n enabledCb: (info: chrome.management.ExtensionInfo) => void;\n disabledCb: (info: chrome.management.ExtensionInfo) => void;\n installedCb: (info: chrome.management.ExtensionInfo) => void;\n uninstalledCb: (extensionId: string) => void;\n}\n\nexport default class Management {\n /**\n * Returns information about the extension. Does not need any permissions.\n */\n static async getExtensionInfo() {\n return await chrome.management.getSelf();\n }\n\n listenForExtensionUpdates(cbs: Partial<ExtensionListeners>) {\n cbs.enabledCb && chrome.management.onEnabled.addListener(cbs.enabledCb);\n cbs.disabledCb && chrome.management.onDisabled.addListener(cbs.disabledCb);\n cbs.installedCb &&\n chrome.management.onInstalled.addListener(cbs.installedCb);\n cbs.uninstalledCb &&\n chrome.management.onUninstalled.addListener(cbs.uninstalledCb);\n }\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"Here are some basic methods:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.management.getSelf()"}),": returns an ",(0,i.jsx)(n.code,{children:"ExtensionInfo"})," object with information about the extension, with properties like these:","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"extension.name"})," : the name of the extension"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"extension.id"})," : the extension id"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"extension.permissions"})," : the array of extension permissions."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"notifications",children:"Notifications"}),"\n",(0,i.jsx)(n.h3,{id:"showing-a-basic-notification",children:"Showing a basic notification"}),"\n",(0,i.jsx)(n.h3,{id:"events",children:"Events"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.notifications.onClicked"})," : when some notification is clicked. Retrieves that notification in a callback."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.notifications.onClosed"})," : when some notification is closed. Retrieves that notification in a callback."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"class-1",children:"Class"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'export default class NotificationModel {\n static showBasicNotification({\n title,\n message,\n iconPath,\n }: {\n title: string;\n message: string;\n iconPath: string;\n }) {\n chrome.notifications.create({\n message,\n title: title,\n type: "basic",\n iconUrl: iconPath,\n });\n }\n\n constructor(private notificationId: string) {}\n\n showNotification(\n options: chrome.notifications.NotificationOptions<true>,\n cb?: (notificationId: string) => void\n ) {\n chrome.notifications.create(this.notificationId, options, cb);\n }
1\n\n clearNotification() {\n chrome.notifications.clear(this.notificationId);\n }\n}\n\nexport class NotificationAPI {\n static show = chrome.notifications.create.bind(chrome.notifications);\n\n static onClick = chrome.notifications.onClicked.addListener.bind(\n chrome.notifications.onClicked\n );\n\n static onClose = chrome.notifications.onClosed.addListener.bind(\n chrome.notifications.onClosed\n );\n\n static onShowSettings = chrome.notifications.onShowSettings.addListener.bind(\n chrome.notifications.onShowSettings\n );\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"permissions",children:"Permissions"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"permissions"})," API does not need any manifest permissions. It allows you to programmatically define and request optional API permissions and host permissions."]}),"\n",(0,i.jsx)(n.p,{children:"You can declare optional permissions in the manifest like so:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'{\n "name": "My extension",\n ...\n "optional_permissions": ["tabs"],\n "optional_host_permissions": ["https://www.google.com/"],\n ...\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["If you want to request hosts that you only discover at runtime, include ",(0,i.jsx)(n.code,{children:"https://*/*"}),' in your extension\'s optional_host_permissions field. This lets you specify any origin in "Permissions.origins" as long as it has a matching scheme.']})}),"\n",(0,i.jsx)(n.h3,{id:"docs",children:"Docs"}),"\n",(0,i.jsx)(n.p,{children:"Here is the basic use case:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"async function requestPermission() {\n\tconst isGranted = await chrome.permissions.request({\n\t permissions: ['tabs'],\n\t origins: ['https://www.google.com/']\n\t})\n\treturn isGranted;\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"permissions"})," object that you pass into these methods looks like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"interface Permissions {\n\tpermissions: string[]\n\torigins: string[]\n}\n"})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.permissions.request(permissions)"})," : requests the specified chrome API permissions and host permissions. You can only use this from a UI gesture like clicking a button. Returns a ",(0,i.jsx)(n.strong,{children:"boolean"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.permissions.contains(permissions)"})," : returns true if the specified permissions are already granted."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.permissions.remove(permissions)"})," : removes the specified permissions. You should always remove permissions once you are done using them. They will be automatically granted next time."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"class-2",children:"Class"}),"\n",(0,i.jsx)(n.p,{children:"Here is a class I made:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"export default class PermissionsModel {\n constructor(public permissions: chrome.permissions.Permissions) {}\n\n async request() {\n return await chrome.permissions.request(this.permissions);\n }\n\n async permissionIsGranted() {\n return await chrome.permissions.contains(this.permissions);\n }\n\n async remove() {\n return await chrome.permissions.remove(this.permissions);\n }\n\n static async getAllOptionalPermissions() {\n const permissions = await chrome.permissions.getAll();\n return permissions.permissions;\n }\n\n static async getAllOptionalHostPermissions() {\n const permissions = await chrome.permissions.getAll();\n return permissions.origins;\n }\n\n static onPermissionsAdded = chrome.permissions.onAdded.addListener.bind(\n chrome.permissions.onAdded\n );\n static onPermissionsRemoved = chrome.permissions.onRemoved.addListener.bind(\n chrome.permissions.onRemoved\n );\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"readinglist",children:"ReadingList"}),"\n",(0,i.jsx)(n.h3,{id:"types",children:"Types"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"declare namespace chrome.readingList {\n export function query(\n options: Partial<chrome.readingList.EntryOptions>\n ): Promise<chrome.readingList.ReadingListEntry[]>;\n\n export function addEntry(\n options: chrome.readingList.EntryOptions\n ): Promise<void>;\n\n export function removeEntry(options: { url: string }): Promise<void>;\n\n export function updateEntry(\n options: Partial<chrome.readingList.EntryOptions>\n ): Promise<void>;\n\n // how you add, search, and update bookmarks\n export interface EntryOptions {\n hasBeenRead: boolean;\n title: string;\n url: string;\n }\n\n // what an article from the reading list looks like\n export interface ReadingListEntry {\n title: string;\n url: string;\n hasBeenRead: boolean;\n creationTime: number;\n lastUpdateTime: number;\n }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"api",children:"API"}),"\n",(0,i.jsx)(n.p,{children:"All methods are async."}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.readingList.query(entryOptions)"})," : gets back all matching articles"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.readingList.addEntry(entryOptions)"})," : adds an article to the reading list"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.readingList.removeEntry({url: string})"})," : removes an article from the reading list based on its URL"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.readingList.updateEntry(entryOptions)"})," : updates an article from the reading list"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"export default class ReadingList {\n static async getAll() {\n return await chrome.readingList.query({});\n }\n\n static async getEntry(options: Partial<chrome.readingList.EntryOptions>) {\n return await chrome.readingList.query(options);\n }\n\n static async addEntry(entry: chrome.readingList.EntryOptions) {\n await chrome.readingList.addEntry(entry);\n }\n\n static async removeEntry(url: string) {\n await chrome.readingList.removeEntry({ url });\n }\n\n static async updateEntry(\n entry: Partial<chrome.readingList.EntryOptions> & { url: string }\n ) {\n await chrome.readingList.updateEntry(entry);\n }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"runtime",children:"Runtime"}),"\n",(0,i.jsx)(n.h3,{id:"opening-a-uninstall-page",children:"Opening a uninstall page"}),"\n",(0,i.jsx)(n.p,{children:"When the user uninstalls the extension, you can use this code to automatically open a new tab begging them to not uninstall your extension."}),"\n",(0,i.jsxs)(n.p,{children:["You do it with the ",(0,i.jsx)(n.code,{children:"chrome.runtime.setUninstallURL(url)"})," method, which will open the specified URL you pass in once the user deletes your extension."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"chrome.runtime.onInstalled.addListener(details => {\n if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {\n chrome.runtime.setUninstallURL('https://example.com/extension-survey');\n }\n});\n"})}),"\n",(0,i.jsx)(n.h3,{id:"api-1",children:"API"}),"\n",(0,i.jsx)(n.p,{children:"Here are the properties:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.runtime.id"})," : the id of the extension"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Here are the methods:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.runtime.getManifest()"})," : non-async. Returns the manifest as an object"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Here are the events:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.runtime.onInstalled"})," : runs when the extension is first installed, refreshed, or chrome updates."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"scripting",children:"scripting"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"scripting"})," API allows you to dynamically inject scripts and CSS into a web page. Scripting requires both the ",(0,i.jsx)(n.code,{children:"scripting"})," permission, and some other permission that lets you gain sensitive access to a website, like ",(0,i.jsx)(n.code,{children:"tabs"}),", ",(0,i.jsx)(n.code,{children:"host_permissions"}),", or ",(0,i.jsx)(n.code,{children:"activeTab"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"When injecting javascript, you can do two thing:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsx)(n.li,{children:"Inject a JS file to run in context of webpage"}),"\n",(0,i.jsx)(n.li,{children:"Inject a JS function to run in context of webpage."}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:" chrome.scripting.executeScript({\n target: { tabId: tab.id }
1,\n files: ['content-script.js']\n });\n"})}),"\n",(0,i.jsx)(n.p,{children:"Here is a list of the options you pass in:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"target"})," : required. AN obejct where we specify the id of the tab to inject the content script into."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"files"})," : a list of content script files to inject into the target. Note that we shoudl refer to our compiled javascript here, not typescript if we're using it."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"func"})," : a function to inject into the target. In this function, we can use DOM code and anything the content script can access."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"args"})," : if injecting content scripts via the function route, this is a list of arguments to pass in to that function."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"injecting-function-example",children:"Injecting function example"}),"\n",(0,i.jsxs)(n.p,{children:["You can inject functions by specifying the ",(0,i.jsx)(n.code,{children:"func"})," and ",(0,i.jsx)(n.code,{children:"args"})," keys in the ",(0,i.jsx)(n.code,{children:"chrome.scripting.executeScript()"})," method options."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:'// needs one argument\nfunction injectedFunction(color) {\n document.body.style.backgroundColor = color;\n}\n\nchrome.action.onClicked.addListener((tab) => {\n chrome.scripting.executeScript({\n target : {tabId : tab.id},\n func : injectedFunction,\n // specify the arguments with the args array\n args : [ "orange" ],\n });\n});\n'})}),"\n",(0,i.jsx)(n.p,{children:"You can also handle a result by returning something from the injected function."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"function getTabId() { ... }\nfunction getTitle() { return document.title; }\n\nchrome.scripting\n .executeScript({\n target : {tabId : getTabId(), allFrames : true},\n func : getTitle,\n })\n .then(injectionResults => {\n for (const {frameId, result} of injectionResults) {\n console.log(`Frame ${frameId} result:`, result);\n }\n });\n"})}),"\n",(0,i.jsx)(n.h3,{id:"full-api",children:"Full API"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'chrome.scripting\n .registerContentScripts([{\n id: "session-script",\n js: ["content.js"],\n persistAcrossSessions: false,\n matches: ["*://example.com/*"],\n runAt: "document_start",\n }])\n .then(() => console.log("registration complete"))\n .catch((err) => console.warn("unexpected error", err))\n\n// update the list of registered content scripts\nchrome.scripting\n .updateContentScripts([{\n id: "session-script",\n excludeMatches: ["*://admin.example.com/*"],\n }])\n .then(() => console.log("registration updated"));\n\n// get back the list of registered content scripts\nchrome.scripting\n .getRegisteredContentScripts()\n .then(scripts => console.log("registered content scripts", scripts));\n\n// unregister the content scripts\nchrome.scripting\n .unregisterContentScripts({ ids: ["session-script"] })\n .then(() => console.log("un-registration complete"));\n'})}),"\n",(0,i.jsx)(n.h3,{id:"class-3",children:"Class"}),"\n",(0,i.jsx)(n.h2,{id:"tabs",children:"Tabs"}),"\n",(0,i.jsx)(n.h3,{id:"permissions-1",children:"Permissions"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:'"tabs"'})," permission in the manifest allows devlopers to access these 4 sensitive properties of any tab: ",(0,i.jsx)(n.code,{children:"url"}),",\xa0",(0,i.jsx)(n.code,{children:"pendingUrl"}),",\xa0",(0,i.jsx)(n.code,{children:"title"}),", and\xa0",(0,i.jsx)(n.code,{children:"favIconUrl"}),". Only use the ",(0,i.jsx)(n.code,{children:"tabs"})," permission if you need these properties. Otherwise, you have full access to the API."]}),"\n",(0,i.jsxs)(n.p,{children:["Host permissions go a bit further, allowing access to sensitive methods like ",(0,i.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/reference/api/tabs#method-captureVisibleTab",children:(0,i.jsx)(n.code,{children:"tabs.capture
1VisibleTab()"})}),",\xa0",(0,i.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/reference/api/tabs#method-executeScript",children:(0,i.jsx)(n.code,{children:"tabs.executeScript()"})}),",\xa0",(0,i.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/reference/api/tabs#method-insertCSS",children:(0,i.jsx)(n.code,{children:"tabs.insertCSS()"})}),", and\xa0",(0,i.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/reference/api/tabs#method-removeCSS",children:(0,i.jsx)(n.code,{children:"tabs.removeCSS()"})})," as well as the 4 sensitive tab properties."]}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"activeTab"})," permission provides temporary host permissions access but without any permission warnings."]}),"\n",(0,i.jsx)(n.p,{children:"All in all, here are the pros and cons of each permission:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:(0,i.jsx)(n.code,{children:"tabs"})}),(0,i.jsx)(n.th,{children:(0,i.jsx)(n.code,{children:"hostPermissions"})}),(0,i.jsx)(n.th,{children:(0,i.jsx)(n.code,{children:"activeTab"})})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Provides access to sensitive tab properties. Has permission warnings."}),(0,i.jsx)(n.td,{children:"Provides access to sensitive tab properties and methods. Has permission warnings"}),(0,i.jsx)(n.td,{children:"Acts as having temporary host permissions. Has permission warnings."})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"api-2",children:"API"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.create(options)"})," : creates a tab. You can specify the tab position, whether it's pinned, the URL, etc."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.query(options)"})," : gets back all matching tabs based on the criteria you specify"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.getCurrent()"})," : returns the tab that the script is running from. Only works for things that use or have UI, like popups or content scripts - not service workers."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.get(tabId)"})," : gets back the tab with the matching tab id"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.move(tabId, options)"})," : moves the tab to specified tab position and optionally window."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.update(tabId, options)"})," : updates the specified tab, doing stuff like pinning, moving, and muting."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.reload(tabId, options)"})," : refreshes the tab. You can also specify options to bypass the cache"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"And here are the event listeners:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onUpdated"}),": When the state of some tab is changed/updated. This runs after ",(0,i.jsx)(n.code,{children:"onActivated"}),", when the URL is available"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onCreated"}),": When a new tab is created."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onActivated"}),": When the active tab in a window changes. The ",(0,i.jsx)(n.code,{children:"url"})," property might not be available yet"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onHighlighted"}),": When the user selects a different tab."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onMoved"})," : When a tab is moved to a different permission."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onAttached"})," : When a tab is moved to a different window."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onDetached"})," : When a tab is moved out of a window."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onRemoved"})," : When a tab is closed."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.tabs.onReplaced"})," : When a tab is closed."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["This is what a ",(0,i.jsx)(n.code,{children:"chrome.tabs.Tab"})," instance looks like:"]}
1),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"interface Tab {\n\tid? : number; \n\t\n\tactive: boolean \n\taudible: boolean \n\tdiscarded: boolean\n\tincognito: boolean\n\tpinned: boolean\n\thighlighted: boolean\n\t\n\tindex: number\n\twindowId: number\n\t\n\tfaviconUrl: string\n\ttitle: string\n\turl: string\n\t\n}\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Properties"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.id"})}),(0,i.jsx)(n.td,{children:"the tab's id"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.active"})}),(0,i.jsx)(n.td,{children:"Whether or not the tab is active"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.audible"})}),(0,i.jsx)(n.td,{children:"Whether or not the tab has recently played audio"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.discarded"})}),(0,i.jsx)(n.td,{children:"Whether or not the tab has been discarded from memory"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.faviconUrl"})}),(0,i.jsx)(n.td,{children:"The favicon URL of the tab. Requires elevated permissions."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.incognito"})}),(0,i.jsx)(n.td,{children:"Whether or not the tab is in an incognito window"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.index"})}),(0,i.jsx)(n.td,{children:"the position of the tab"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.pinned"})}),(0,i.jsx)(n.td,{children:"Whether or not the tab is pinned"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.title"})}),(0,i.jsx)(n.td,{children:"The tab's title. Requires elevated permissions."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.url"})}),(0,i.jsx)(n.td,{children:"The tab's URL. Requires elevated permissions."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.windowId"})}),(0,i.jsx)(n.td,{children:"The id of the tab's containing window"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"tab.highlighted"})}),(0,i.jsx)(n.td,{children:"Whether or not the tab is currently selected."})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"use-cases",children:"Use cases"}),"\n",(0,i.jsx)(n.h4,{id:"creating-a-tab",children:"Creating a tab"}),"\n",(0,i.jsx)(n.h4,{id:"querying-tabs",children:"Querying tabs"}),"\n",(0,i.jsx)(n.h4,{id:"screenshots",children:"Screenshots"}),"\n",(0,i.jsx)(n.h4,{id:"dealing-with-page-zoom",children:"Dealing with page zoom"}),"\n",(0,i.jsx)(n.h4,{id:"grouping-tabs",children:"Grouping tabs"}),"\n",(0,i.jsx)(n.h4,{id:"listening-for-tab-changes",children:"Listening for tab changes"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {\n if (changeInfo.status === "complete") {\n // tab navigation/reload is complete. do something\n }\n});\n'})}),"\n",(0,i.jsx)(n.h2,{id:"tabgroups",children:"TabGroups"}),"\n",(0,i.jsx)(n.h2,{id:"userscripts",children:"userScripts"}),"\n",(0,i.jsx)(n.p,{children:"user scripts are a way to automatically run javascript code on a webpage whenever the user navigates to it. Users can define their own javascript code they want to run that will get around the content security policy."}),"\n",(0,i.jsx)(n.p,{children:"Each user script has three parts to it:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"id"}),": the unique identifier for the script"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"url matches"}),": the url blob patterns to match, meaning the script will run on any urls that match one from the list of patterns"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"code"}),": the script code. This can be a string or a reference to a file."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"setup",children:"Setup"}),"\n",(0,i.jsx)(n.p,{children:"For user scripts to work, host permissions for select match patterns have to be enabled and the user must have their chrome extensions page in developer mode."}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Add the ",(0,i.jsx)(n.code,{children:'"userScripts"'})," permission to the manifest"]}),"\n",(0,i.jsx)(n.li,{children:"Add host permissions for whichever match patterns you are going to have user scripts running on. This can also be optional host permissions you can request at runtime."}),"\n",(0,i.jsx)(n.li,{children:"Check if user scripts exist"}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"function isUserScriptsAvailable() {\n try {\n // Property access which throws if developer mode is not enabled.\n const thing = chrome.userScripts === undefined;\n if (thing) {\n return false;\n }\n return true;\n } catch {\n // Not available.\n return false;\n }\n }\n"})}),"\n",(0,i.jsxs)(n.ol,{start:"4",children:["\n",(0,i.jsx)(n.li,{children:"Configure user script CSP"}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"function setWorld() {\n chrome.userScripts.configureWorld({\n csp: \"script-src 'self'\",\n });\n }\n"})}),"\n",(0,i.jsx)(n.p,{children:"For the optional host permissions, any url match you request for the user script must be a subset of the match patterns from the optional host permissions, so it's better to just go as broad as possible:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "permissions": ["storage", "userScripts", "unlimitedStorage"],\n "optional_host_permissions": ["<all_urls>"],\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"You can then ask for permission and then register like so:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:" async function registerScript (url: string, code: string) => {\n\t // create new script representation\n const currentScript = new UserScripts(crypto.randomUUID());\n const domainUrl = new URL(url).origin;\n // create glob match pattern\n const urlMatch = `${domainUrl}/*`;\n\n\t\t// request glob pattern as optional host permission\n const permissions = new PermissionsModel({\n origins: [urlMatch],\n });\n\t // request permission\n const isGranted = await permissions.request();\n if (!isGranted) {\n return;\n }\n\n await currentScript.registerScript([urlMatch], code);\n }\n"})}),"\n",(0,i.jsx)(n.h3,{id:"api-3",children:"API"}),"\n",(0,i.jsx)(n.p,{children:"All of these methods are asynchronous."}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.userScripts.register(options)"}),": registers a user script. Throws an error if you try to register an existing script with the same id. Here are the keys of options object:","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"id"}),": the script id"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"matches"}),": a string array of glob patterns that the script will run on if the current tab url matches one of those patterns"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"js"}),": the pieces of code to run. This can be strings or files."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.userScripts.unregister(options)"}),": unregisters the specified user scripts based on their ids."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.userScripts.update(scripts)"}),": updates the specified user scripts. ",(0,i.jsx)(n.code,{children:"scripts"})," is an array of objects, which each have these keys:","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"id"}),": the script id"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"matches"}),": a string array of glob patterns that the script will run on if the current tab url matches one of those patterns"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"js"}),": the pieces of code to run. This can be strings or files."]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["To get a script, use ",(0,i.jsx)(n.code,{children:"chrome.userScripts.getScript()"})," method, which if you pass in nothing, it returns all user scripts, but you can also pass in a filter to filter off all ids."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'export class UserScripts {\n static isUserScriptsAvailable() {\n try {\n // Property access which throws if developer mode is not enabled.\n const thing = chrome.userScripts === undefined;\n if (thing) {\n return false;\n }\n return true;\n } catch {\n // Not available.\n return false;\n }\n }\n\n static async getAllScripts() {\n const scripts = await chrome.userScripts.getScripts();\n return scripts;\n }\n\n static setWorld() {\n chrome.userScripts.configureWorld({\n csp: "script-src \'self\'",\n });\n }\n\n constructor(public readonly id: string) {}\n\n async registerScript(matchPatterns: string[], js: string) {\n if (await this.get
1Script()) {\n return;\n }\n console.log("registering script...");\n await chrome.userScripts.register([\n {\n id: this.id,\n matches: matchPatterns,\n js: [{ code: js }],\n world: "USER_SCRIPT",\n },\n ]);\n }\n\n async getScript() {\n const [script] = await chrome.userScripts.getScripts({\n ids: [this.id],\n });\n if (!script) {\n return null;\n }\n return script;\n }\n\n async unregisterScript() {\n const script = await this.getScript();\n if (!script) {\n return;\n }\n await chrome.userScripts.unregister({\n ids: [this.id],\n });\n }\n\n async updateScript(js: string) {\n await chrome.userScripts.update([\n {\n id: this.id,\n js: [{ code: js }],\n },\n ]);\n }\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"the-major-caveat",children:"The major caveat"}),"\n",(0,i.jsx)(n.p,{children:"User Scripts are all automatically deregistered whenever the extension updates, so it's best to store representations of your user scripts somehow in storage and then re-register them all when the extension updates."}),"\n",(0,i.jsx)(n.h2,{id:"webnavigation",children:"webNavigation"}),"\n",(0,i.jsxs)(n.p,{children:["Here are the events you can listen to in the ",(0,i.jsx)(n.code,{children:"webNavigation"})," API."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.webNavigation.onCompleted"})," : when the user finishes navigating to a new tab and the DOM is fully loaded."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.webNavigation.onBeforeNavigate"})," : when the user beings navigating to a new tab"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"chrome.webNavigation.onCompleted.addListener(({ url, tabId }) => {\n console.log(`Tab ${tabId} has finished loading ${url}`);\n});\n\nchrome.webNavigation.onBeforeNavigate.addListener(({ url, tabId }) => {\n console.log(`Before navigating to ${url} in tab ${tabId}`);\n});\n"})}),"\n",(0,i.jsx)(n.h2,{id:"windows",children:"Windows"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"chrome.windows.create(options)"})," : an async function that creates a window given some options, which have the following keys:","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"url?"})," : the array of urls to open as tabs in the new window"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:'export default class Windows {\n static async createBasicWindow(urls?: string[]) {\n return await chrome.windows.create({\n url: urls,\n focused: true,\n state: "fullscreen",\n });\n }\n}\n'})})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8453:(e,n,s)=>{s.d(n,{R:()=>o,x:()=>a});var i=s(6540);const t={},r=i.createContext(t);function o(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.