Реализация Context Menu (контекстное меню) в браузерном расширении
Контекстное меню браузера — быстрый способ дать пользователю действие прямо на выделенном тексте, ссылке или изображении без открытия popup. API простое, но есть несколько нюансов с жизненным циклом и вложенностью.
Разрешение в манифесте
{
"permissions": ["contextMenus"]
}
Без этого разрешения вызовы chrome.contextMenus молча игнорируются.
Создание пунктов меню
Пункты меню создаются в service worker при установке расширения. Если создавать их при каждом старте SW — получите дубликаты, потому что браузер не очищает их автоматически:
chrome.runtime.onInstalled.addListener(() => {
// Чистим старые пункты перед созданием новых
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: 'translate-selection',
title: 'Перевести "%s"',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'save-link',
title: 'Сохранить ссылку в список',
contexts: ['link'],
});
chrome.contextMenus.create({
id: 'search-image',
title: 'Поиск по изображению',
contexts: ['image'],
});
// Разделитель
chrome.contextMenus.create({
id: 'separator-1',
type: 'separator',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'copy-clean',
title: 'Копировать без форматирования',
contexts: ['selection'],
});
});
});
%s в поле title заменяется на выделенный текст (до ~25 символов, браузер обрезает сам).
Типы контекстов
| Контекст | Срабатывает |
|---|---|
selection |
есть выделенный текст |
link |
правый клик по ссылке |
image |
правый клик по изображению |
video / audio |
медиа-элементы |
editable |
поля ввода, textarea |
page |
любое место на странице |
all |
везде |
Обработка кликов
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
switch (info.menuItemId) {
case 'translate-selection':
await handleTranslate(info.selectionText, tab);
break;
case 'save-link':
await handleSaveLink(info.linkUrl, info.pageUrl, tab);
break;
case 'search-image':
await handleImageSearch(info.srcUrl, tab);
break;
case 'copy-clean':
// Выполняем код в контексте страницы
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (text) => navigator.clipboard.writeText(text),
args: [info.selectionText]
});
break;
}
});
async function handleTranslate(text, tab) {
const { targetLang = 'ru' } = await chrome.storage.sync.get('targetLang');
const response = await fetch(
`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=auto|${targetLang}`
);
const data = await response.json();
// Показываем результат в content script
await chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_TRANSLATION',
original: text,
translated: data.responseData.translatedText
});
}
Вложенные меню
chrome.contextMenus.create({
id: 'parent-send-to',
title: 'Отправить в...',
contexts: ['selection', 'link'],
});
chrome.contextMenus.create({
id: 'send-to-telegram',
parentId: 'parent-send-to',
title: 'Telegram',
contexts: ['selection', 'link'],
});
chrome.contextMenus.create({
id: 'send-to-notion',
parentId: 'parent-send-to',
title: 'Notion',
contexts: ['selection', 'link'],
});
Динамическое обновление пунктов
Пункт меню можно обновить без пересоздания — удобно для отображения текущего состояния:
async function updateMenuItemState() {
const { enabled } = await chrome.storage.sync.get('enabled');
chrome.contextMenus.update('toggle-feature', {
title: enabled ? 'Отключить подсветку' : 'Включить подсветку',
checked: enabled // для type: 'checkbox'
});
}
// Вызываем при изменении настроек
chrome.storage.onChanged.addListener((changes) => {
if ('enabled' in changes) updateMenuItemState();
});
Меню для конкретных сайтов
documentUrlPatterns ограничивает пункт определёнными URL:
chrome.contextMenus.create({
id: 'github-open-pr',
title: 'Открыть PR в новой вкладке',
contexts: ['link'],
documentUrlPatterns: ['https://github.com/*'],
targetUrlPatterns: ['https://github.com/*/pull/*']
});
Контекстное меню — минимально инвазивный UX: пользователь вызывает действие сам, расширение не навязывается.







