Bug 1250631 - Implement chrome.contextMenus.onClicked. r=kmag
authorMatthew Wein <mwein@mozilla.com>
Wed, 23 Mar 2016 02:39:10 -0700
changeset 291300 6934dc0c95b8657235da70eb0a37a511998944be
parent 291299 5851bac416b3be028afc426f6f8f7032c89aa2fb
child 291301 7de386dfe2bb8b3014a4ed51c8ee89de5659cbb9
push id30131
push userkwierso@gmail.com
push dateFri, 01 Apr 2016 22:43:45 +0000
treeherdermozilla-central@c40c0b2f3b4c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1250631
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1250631 - Implement chrome.contextMenus.onClicked. r=kmag MozReview-Commit-ID: Ic7opjJtmRB
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/test/browser/browser_ext_contextMenus.js
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -15,19 +15,16 @@ var {
 // Map[Extension -> Map[ID -> MenuItem]]
 // Note: we want to enumerate all the menu items so
 // this cannot be a weak map.
 var gContextMenuMap = new Map();
 
 // Map[Extension -> MenuItem]
 var gRootItems = new Map();
 
-// Not really used yet, will be used for event pages.
-var gOnClickedCallbacksMap = new WeakMap();
-
 // If id is not specified for an item we use an integer.
 var gNextMenuItemID = 0;
 
 // Used to assign unique names to radio groups.
 var gNextRadioGroupID = 0;
 
 // The max length of a menu item's label.
 var gMaxLabelLength = 64;
@@ -163,33 +160,39 @@ var gMenuBuilder = {
       }
     }
 
     if (!item.enabled) {
       element.setAttribute("disabled", "true");
     }
 
     element.addEventListener("command", event => {  // eslint-disable-line mozilla/balanced-listeners
+      if (event.target !== event.currentTarget) {
+        return;
+      }
       if (item.type == "checkbox") {
         item.checked = !item.checked;
       } else if (item.type == "radio") {
         // Deselect all radio items in the current radio group.
         for (let child of item.parent.children) {
           if (child.type == "radio" && child.groupName == item.groupName) {
             child.checked = false;
           }
         }
         // Select the clicked radio item.
         item.checked = true;
       }
 
       item.tabManager.addActiveTabPermission();
+
+      let tab = item.tabManager.convert(contextData.tab);
+      let info = item.getClickInfo(contextData, event);
+      item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
       if (item.onclick) {
-        let clickData = item.getClickData(contextData, event);
-        runSafe(item.extContext, item.onclick, clickData);
+        runSafe(item.extContext, item.onclick, info, tab);
       }
     });
 
     return element;
   },
 
   handleEvent: function(event) {
     if (this.xulMenu != event.target || event.type != "popuphidden") {
@@ -385,52 +388,48 @@ MenuItem.prototype = {
 
     let menuMap = gContextMenuMap.get(this.extension);
     menuMap.delete(this.id);
     if (this.root == this) {
       gRootItems.delete(this.extension);
     }
   },
 
-  getClickData(contextData, event) {
+  getClickInfo(contextData, event) {
     let mediaType;
     if (contextData.onVideo) {
       mediaType = "video";
     }
     if (contextData.onAudio) {
       mediaType = "audio";
     }
     if (contextData.onImage) {
       mediaType = "image";
     }
 
-    let clickData = {
+    let info = {
       menuItemId: this.id,
     };
 
     function setIfDefined(argName, value) {
       if (value) {
-        clickData[argName] = value;
+        info[argName] = value;
       }
     }
 
-    let tab = contextData.tab ? TabManager.convert(this.extension, contextData.tab)
-                              : undefined;
-
     setIfDefined("parentMenuItemId", this.parentId);
     setIfDefined("mediaType", mediaType);
     setIfDefined("linkUrl", contextData.linkUrl);
     setIfDefined("srcUrl", contextData.srcUrl);
     setIfDefined("pageUrl", contextData.pageUrl);
     setIfDefined("frameUrl", contextData.frameUrl);
     setIfDefined("selectionText", contextData.selectionText);
     setIfDefined("editable", contextData.onEditableArea);
-    setIfDefined("tab", tab);
 
-    return clickData;
+    return info;
   },
 
   enabledForContext(contextData) {
     let contexts = getContexts(contextData);
     if (!this.contexts.some(n => contexts.has(n))) {
       return false;
     }
 
@@ -502,22 +501,21 @@ extensions.registerSchemaAPI("contextMen
       removeAll: function() {
         let root = gRootItems.get(extension);
         if (root) {
           root.remove();
         }
         return Promise.resolve();
       },
 
-      // TODO: implement this once event pages are ready.
       onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
-        let callback = menuItem => {
-          fire(menuItem.data);
+        let listener = (event, info, tab) => {
+          fire(info, tab);
         };
 
-        gOnClickedCallbacksMap.set(extension, callback);
+        extension.on("webext-contextmenu-menuitem-click", listener);
         return () => {
-          gOnClickedCallbacksMap.delete(extension);
+          extension.off("webext-contextmenu-menuitem-click", listener);
         };
       }).api(),
     },
   };
 });
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -12,20 +12,24 @@ add_task(function* () {
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["contextMenus"],
     },
 
     background: function() {
       // A generic onclick callback function.
-      function genericOnClick(info) {
-        browser.test.sendMessage("menuItemClick", info);
+      function genericOnClick(info, tab) {
+        browser.test.sendMessage("onclick", {info, tab});
       }
 
+      browser.contextMenus.onClicked.addListener((info, tab) => {
+        browser.test.sendMessage("browser.contextMenus.onClicked", {info, tab});
+      });
+
       browser.contextMenus.create({
         contexts: ["all"],
         type: "separator",
       });
 
       let contexts = ["page", "selection", "image"];
       for (let i = 0; i < contexts.length; i++) {
         let context = contexts[i];
@@ -34,19 +38,19 @@ add_task(function* () {
           title: title,
           contexts: [context],
           id: "ext-" + context,
           onclick: genericOnClick,
         });
         if (context == "selection") {
           browser.contextMenus.update("ext-selection", {
             title: "selection is: '%s'",
-            onclick: (info) => {
+            onclick: (info, tab) => {
               browser.contextMenus.removeAll();
-              genericOnClick(info);
+              genericOnClick(info, tab);
             },
           });
         }
       }
 
       let parent = browser.contextMenus.create({
         title: "parent",
       });
@@ -75,60 +79,59 @@ add_task(function* () {
         onclick: genericOnClick,
       });
       browser.contextMenus.remove(parentToDel);
 
       browser.contextMenus.create({
         title: "radio-group-1",
         type: "radio",
         checked: true,
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "radio-group-2",
         type: "radio",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "radio-group-2",
         type: "radio",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         type: "separator",
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
         checked: true,
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
+      browser.contextMenus.create({
+        title: "Without onclick property",
+        id: "ext-without-onclick",
+      });
+
       browser.contextMenus.update(parent, {parentId: child2}).then(
         () => {
           browser.test.notifyFail();
         },
         () => {
           browser.test.notifyPass();
         }
       );
@@ -157,30 +160,39 @@ add_task(function* () {
     }, gBrowser.selectedBrowser);
     yield popupShownPromise;
 
     popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
     EventUtils.synthesizeMouseAtCenter(getTop(), {});
     yield popupShownPromise;
   }
 
-  function* closeContextMenu(itemToSelect, expectedClickInfo) {
-    function checkClickInfo(info) {
+  function* closeContextMenu(itemToSelect, expectedClickInfo, hasOnclickProperty = true) {
+    function checkClickInfo(info, tab) {
       for (let i of Object.keys(expectedClickInfo)) {
         is(info[i], expectedClickInfo[i],
            "click info " + i + " expected to be: " + expectedClickInfo[i] + " but was: " + info[i]);
       }
-      is(expectedClickInfo.pageSrc, info.tab.url);
+      is(expectedClickInfo.pageSrc, tab.url);
     }
     let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
     EventUtils.synthesizeMouseAtCenter(itemToSelect, {});
-    let clickInfo = yield extension.awaitMessage("menuItemClick");
+
+    if (hasOnclickProperty) {
+      let {info, tab} = yield extension.awaitMessage("onclick");
+      if (expectedClickInfo) {
+        checkClickInfo(info, tab);
+      }
+    }
+
+    let {info, tab} = yield extension.awaitMessage("browser.contextMenus.onClicked");
     if (expectedClickInfo) {
-      checkClickInfo(clickInfo);
+      checkClickInfo(info, tab);
     }
+
     yield popupHiddenPromise;
   }
 
   function confirmRadioGroupStates(expectedStates) {
     let top = getTop();
 
     let radioItems = top.getElementsByAttribute("type", "radio");
     let radioGroup1 = top.getElementsByAttribute("label", "radio-group-1");
@@ -283,16 +295,29 @@ add_task(function* () {
     selection.addRange(range);
   });
 
   // Bring up context menu again
   yield openExtensionMenu();
 
   // Check some menu items
   top = getTop();
+  items = top.getElementsByAttribute("label", "Without onclick property");
+  is(items.length, 1, "contextMenu item was found (context=page)");
+
+  yield closeContextMenu(items[0], {
+    menuItemId: "ext-without-onclick",
+    pageUrl: "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html",
+  }, false /* hasOnclickProperty */);
+
+  // Bring up context menu again
+  yield openExtensionMenu();
+
+  // Check some menu items
+  top = getTop();
   items = top.getElementsByAttribute("label", "selection is: 'just some text 123456789012345678901234567890...'");
   is(items.length, 1, "contextMenu item for selection was found (context=selection)");
   let selectionItem = items[0];
 
   items = top.getElementsByAttribute("label", "selection");
   is(items.length, 0, "contextMenu item label update worked (context=selection)");
 
   yield closeContextMenu(selectionItem, {