UNPKG

webext-active-tab

Version:

WebExtension module: Track `activeTab` permission; automatically inject content scripts

95 lines (94 loc) 3.69 kB
import { isScriptableUrl } from 'webext-content-scripts'; import { isBackground } from 'webext-detect'; import SimpleEventTarget from 'simple-event-target'; import { StorageItemMap } from 'webext-storage'; if (!isBackground()) { throw new Error('This module is only allowed in a background script'); } if (!chrome.runtime.getManifest().permissions?.includes('activeTab')) { throw new Error('The manifest should include the `activeTab` permission for `webext-active-tab` to work'); } const activation = new SimpleEventTarget(); const deactivation = new SimpleEventTarget(); const browserAction = chrome.action ?? chrome.browserAction; /** * The list is not guaranteed to be up to date. The activeTab permission might be lost before it's detected. */ export const possiblyActiveTabs = chrome.storage.session ? new StorageItemMap('webext-active-tab', { area: 'session', }) : new Map(); async function addIfScriptable({ url, id }) { if (id && url // Skip if it already exists. A previous change of origin already cleared this && !await possiblyActiveTabs.has(String(id)) // ActiveTab doesn't make sense on non-scriptable URLs as they generally don't have scriptable frames && isScriptableUrl(url) // Note: Do not filter by `isContentScriptRegistered`; `active-tab` also applies to random `executeScript` calls ) { const { origin } = new URL(url); console.debug('activeTab:', id, 'added', { origin }); await possiblyActiveTabs.set(String(id), origin); activation.emit({ id, origin }); } else { console.debug('activeTab:', id, 'not added', { origin }); } } async function dropIfOriginChanged(tabId, { url }) { if (!url) { return; } const activeOrigin = await possiblyActiveTabs.get(String(tabId)); const { origin } = new URL(url); if (activeOrigin !== origin) { console.debug('activeTab:', tabId, 'removed because origin changed from', activeOrigin, 'to', origin); void possiblyActiveTabs.delete(String(tabId)); } } function altListener(_, tab) { if (tab) { void addIfScriptable(tab); } } function drop(tabId) { console.debug('activeTab:', tabId, 'removed'); void possiblyActiveTabs.delete(String(tabId)); deactivation.emit(tabId); } // https://developer.chrome.com/docs/extensions/mv3/manifest/activeTab/#invoking-activeTab function startActiveTabTracking() { browserAction?.onClicked.addListener(addIfScriptable); chrome.contextMenus?.onClicked.addListener(altListener); chrome.commands?.onCommand.addListener(altListener); chrome.tabs.onUpdated.addListener(dropIfOriginChanged); chrome.tabs.onRemoved.addListener(drop); } function stopActiveTabTracking() { browserAction?.onClicked.removeListener(addIfScriptable); chrome.contextMenus?.onClicked.removeListener(altListener); chrome.commands?.onCommand.removeListener(altListener); chrome.tabs.onUpdated.removeListener(dropIfOriginChanged); chrome.tabs.onRemoved.removeListener(drop); // TODO: Restore after https://github.com/fregante/webext-storage/issues/9 // void possiblyActiveTabs.clear(); } export const PRIVATE = { startActiveTabTracking, stopActiveTabTracking, }; export const onActiveTab = { addListener(callback) { startActiveTabTracking(); activation.subscribe(callback); }, removeListener: activation.unsubscribe.bind(activation), }; export const onActiveTabLost = { addListener(callback) { startActiveTabTracking(); deactivation.subscribe(callback); }, removeListener: deactivation.unsubscribe.bind(deactivation), };