Bug 1370499 - Support WebExtensions bookmark context menus. r=mixedpuppy
MozReview-Commit-ID: AkYxeGHlDvi
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -231,17 +231,19 @@ var gMenuBuilder = {
if (child.type == "radio" && child.groupName == item.groupName) {
child.checked = false;
}
}
// Select the clicked radio item.
item.checked = true;
}
- item.tabManager.addActiveTabPermission();
+ if (!contextData.onBookmark) {
+ item.tabManager.addActiveTabPermission();
+ }
let tab = contextData.tab && item.tabManager.convert(contextData.tab);
let info = item.getClickInfo(contextData, wasChecked);
const map = {shiftKey: "Shift", altKey: "Alt", metaKey: "Command", ctrlKey: "Ctrl"};
info.modifiers = Object.keys(map).filter(key => event[key]).map(key => map[key]);
if (event.ctrlKey && AppConstants.platform === "macosx") {
info.modifiers.push("MacCtrl");
@@ -312,16 +314,17 @@ const contextsMap = {
onEditableArea: "editable",
inFrame: "frame",
onImage: "image",
onLink: "link",
onPassword: "password",
isTextSelected: "selection",
onVideo: "video",
+ onBookmark: "bookmark",
onBrowserAction: "browser_action",
onPageAction: "page_action",
onTab: "tab",
inToolsMenu: "tools_menu",
};
const getMenuContexts = contextData => {
let contexts = new Set();
@@ -332,17 +335,17 @@ const getMenuContexts = contextData => {
}
}
if (contexts.size === 0) {
contexts.add("page");
}
// New non-content contexts supported in Firefox are not part of "all".
- if (!contextData.onTab && !contextData.inToolsMenu) {
+ if (!contextData.onBookmark && !contextData.onTab && !contextData.inToolsMenu) {
contexts.add("all");
}
return contexts;
};
function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
@@ -518,31 +521,36 @@ MenuItem.prototype = {
setIfDefined("mediaType", mediaType);
setIfDefined("linkText", contextData.linkText);
setIfDefined("linkUrl", contextData.linkUrl);
setIfDefined("srcUrl", contextData.srcUrl);
setIfDefined("pageUrl", contextData.pageUrl);
setIfDefined("frameUrl", contextData.frameUrl);
setIfDefined("frameId", contextData.frameId);
setIfDefined("selectionText", contextData.selectionText);
+ setIfDefined("bookmarkId", contextData.bookmarkId);
if ((this.type === "checkbox") || (this.type === "radio")) {
info.checked = this.checked;
info.wasChecked = wasChecked;
}
return info;
},
enabledForContext(contextData) {
let contexts = getMenuContexts(contextData);
if (!this.contexts.some(n => contexts.has(n))) {
return false;
}
+ if (contextData.onBookmark) {
+ return this.extension.hasPermission("bookmarks");
+ }
+
let docPattern = this.documentUrlMatchPattern;
let pageURI = Services.io.newURI(contextData[contextData.inFrame ? "frameUrl" : "pageUrl"]);
if (docPattern && !docPattern.matches(pageURI)) {
return false;
}
let targetPattern = this.targetUrlMatchPattern;
if (targetPattern) {
@@ -561,17 +569,17 @@ MenuItem.prototype = {
return true;
},
};
// While any extensions are active, this Tracker registers to observe/listen
// for menu events from both Tools and context menus, both content and chrome.
const menuTracker = {
- menuIds: ["menu_ToolsPopup", "tabContextMenu"],
+ menuIds: ["placesContext", "menu_ToolsPopup", "tabContextMenu"],
register() {
Services.obs.addObserver(this, "on-build-contextmenu");
for (const window of windowTracker.browserWindows()) {
this.onWindowOpen(window);
}
windowTracker.addOpenListener(this.onWindowOpen);
},
@@ -596,16 +604,28 @@ const menuTracker = {
for (const id of menuTracker.menuIds) {
const menu = window.document.getElementById(id);
menu.addEventListener("popupshowing", menuTracker);
}
},
handleEvent(event) {
const menu = event.target;
+ if (menu.id === "placesContext") {
+ const trigger = menu.triggerNode;
+ if (!trigger._placesNode) {
+ return;
+ }
+
+ gMenuBuilder.build({
+ menu,
+ bookmarkId: trigger._placesNode.bookmarkGuid,
+ onBookmark: true,
+ });
+ }
if (menu.id === "menu_ToolsPopup") {
const tab = tabTracker.activeTab;
const pageUrl = tab.linkedBrowser.currentURI.spec;
gMenuBuilder.build({menu, tab, pageUrl, inToolsMenu: true});
}
if (menu.id === "tabContextMenu") {
const trigger = menu.triggerNode;
const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
--- a/browser/components/extensions/schemas/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -22,17 +22,17 @@
"namespace": "contextMenus",
"permissions": ["contextMenus"],
"description": "Use the browser.contextMenus API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
"$import": "menus",
"types": [
{
"id": "ContextType",
"type": "string",
- "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab"],
+ "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "bookmark", "browser_action", "page_action", "tab"],
"description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
}
]
},
{
"namespace": "menus",
"permissions": ["menus"],
"description": "Use the browser.menus API to add items to the browser's menus. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
@@ -41,17 +41,17 @@
"value": 6,
"description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
}
},
"types": [
{
"id": "ContextType",
"type": "string",
- "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab", "tools_menu"],
+ "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "bookmark", "browser_action", "page_action", "tab", "tools_menu"],
"description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
},
{
"id": "ItemType",
"type": "string",
"enum": ["normal", "checkbox", "radio", "separator"],
"description": "The type of menu item."
},
@@ -119,16 +119,20 @@
"optional": true,
"description": "A flag indicating the state of a checkbox or radio item before it was clicked."
},
"checked": {
"type": "boolean",
"optional": true,
"description": "A flag indicating the state of a checkbox or radio item after it is clicked."
},
+ "bookmarkId": {
+ "type": "string",
+ "description": "The id of the bookmark where the context menu was clicked, if it was on a bookmark."
+ },
"modifiers": {
"type": "array",
"items": {
"type": "string",
"enum": ["Shift", "Alt", "Command", "Ctrl", "MacCtrl"]
},
"description": "An array of keyboard modifiers that were held while the menu item was clicked."
}
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -445,8 +445,84 @@ add_task(async function testRemoveAllWit
// Confirm only gamma is left.
await confirmMenuItems("gamma");
await closeContextMenu();
await first.unload();
await second.unload();
await BrowserTestUtils.removeTab(tab);
});
+
+add_task(async function test_bookmark_contextmenu() {
+ const bookmarksToolbar = document.getElementById("PersonalToolbar");
+ setToolbarVisibility(bookmarksToolbar, true);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus", "bookmarks"],
+ },
+ async background() {
+ const url = "https://example.com/";
+ const title = "Example";
+ let newBookmark = await browser.bookmarks.create({
+ url,
+ title,
+ parentId: "toolbar_____",
+ });
+ await browser.contextMenus.create({
+ title: "Get bookmark",
+ contexts: ["bookmark"],
+ });
+ browser.test.sendMessage("bookmark-created");
+ browser.contextMenus.onClicked.addListener(async (info) => {
+ browser.test.assertEq(newBookmark.id, info.bookmarkId, "Bookmark ID matches");
+
+ let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
+ browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
+ browser.test.assertEq(url, bookmark.url, "Bookmark url matches");
+ browser.test.assertFalse(info.hasOwnProperty("pageUrl"), "Context menu does not expose pageUrl");
+ await browser.bookmarks.remove(info.bookmarkId);
+ browser.test.sendMessage("test-finish");
+ });
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("bookmark-created");
+ let menu = await openChromeContextMenu("placesContext",
+ "#PersonalToolbar .bookmark-item:last-child");
+
+ let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+ closeChromeContextMenu("placesContext", menuItem);
+
+ await extension.awaitMessage("test-finish");
+ await extension.unload();
+ setToolbarVisibility(bookmarksToolbar, false);
+});
+
+add_task(async function test_bookmark_context_requires_permission() {
+ const bookmarksToolbar = document.getElementById("PersonalToolbar");
+ setToolbarVisibility(bookmarksToolbar, true);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ async background() {
+ await browser.contextMenus.create({
+ title: "Get bookmark",
+ contexts: ["bookmark"],
+ });
+ browser.test.sendMessage("bookmark-created");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("bookmark-created");
+ let menu = await openChromeContextMenu("placesContext",
+ "#PersonalToolbar .bookmark-item:last-child");
+
+ Assert.equal(menu.getElementsByAttribute("label", "Get bookmark").length, 0,
+ "bookmark context menu not created with `bookmarks` permission.");
+
+ closeChromeContextMenu("placesContext");
+
+ await extension.unload();
+ setToolbarVisibility(bookmarksToolbar, false);
+});