PageSourceSearch

https://threejsnotes.netlify.app/assets/js/99edc834.7f829db1.js

js threejsnotes.netlify.app collected 2026-10-03 11:32:26 UTC 58,865 bytes, 1 lines download raw bytes

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.