Bug 1350298 - Reduce the complexity of nsContextMenu:setTarget. r?jaws draft
authorMark Banner <standard8@mozilla.com>
Thu, 23 Mar 2017 16:22:24 +0000
changeset 504543 548984166a4562db1e66b96f5038b17f7b03d65a
parent 504542 f929aa1d89d4ae76e55cb35e3e8a3a736f5d55ec
child 504544 e93cbe458d67f61a9f6ab861cec025651394d114
push id50821
push userbmo:standard8@mozilla.com
push dateFri, 24 Mar 2017 12:08:23 +0000
reviewersjaws
bugs1350298
milestone55.0a1
Bug 1350298 - Reduce the complexity of nsContextMenu:setTarget. r?jaws MozReview-Commit-ID: 2TYNVCV4LSL
browser/base/content/nsContextMenu.js
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -569,34 +569,40 @@ nsContextMenu.prototype = {
 
   inspectNode() {
     let gBrowser = this.browser.ownerGlobal.gBrowser;
     let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     let { gDevToolsBrowser } = require("devtools/client/framework/devtools-browser");
     return gDevToolsBrowser.inspectNode(gBrowser.selectedTab, this.target);
   },
 
-  // Set various context menu attributes based on the state of the world.
+  /**
+   * Set various context menu attributes based on the state of the world.
+   * Note: If the context menu is on a remote process the supplied parameters
+   * will be overwritten with data from gContextMenuContentData.
+   *
+   * @param {Object} aNode The node that this menu is being opened on.
+   * @param {nsIDOMNode} aRangeParent The parent node for where the selection ends.
+   * @param {Integer} aRangeOffset The end position of where the selction ends.
+   */
   setTarget(aNode, aRangeParent, aRangeOffset) {
     // gContextMenuContentData.isRemote tells us if the event came from a remote
     // process. gContextMenuContentData can be null if something (like tests)
     // opens the context menu directly.
-    let editFlags;
     this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
     if (this.isRemote) {
       aNode = gContextMenuContentData.event.target;
       aRangeParent = gContextMenuContentData.event.rangeParent;
       aRangeOffset = gContextMenuContentData.event.rangeOffset;
-      editFlags = gContextMenuContentData.editFlags;
     }
 
     const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     if (aNode.nodeType == Node.DOCUMENT_NODE ||
         // Not display on XUL element but relax for <label class="text-link">
-        (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) {
+        (aNode.namespaceURI == xulNS && !this._isXULTextLinkLabel(aNode))) {
       this.shouldDisplay = false;
       return;
     }
 
     // Initialize contextual info.
     this.onImage           = false;
     this.onLoadedImage     = false;
     this.onCompletedImage  = false;
@@ -641,22 +647,25 @@ nsContextMenu.prototype = {
     this.isTextSelected    = this.textSelected.length != 0;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
     let ownerDoc = this.target.ownerDocument;
     this.ownerDoc = ownerDoc;
 
+    let editFlags;
+
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
       this.browser = gContextMenuContentData.browser;
       this.principal = gContextMenuContentData.principal;
       this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID;
+      editFlags = gContextMenuContentData.editFlags;
     } else {
       editFlags = SpellCheckHelper.isEditable(this.target, window);
       this.browser = ownerDoc.defaultView
                              .QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIWebNavigation)
                              .QueryInterface(Ci.nsIDocShell)
                              .chromeEventHandler;
       this.principal = ownerDoc.nodePrincipal;
@@ -664,124 +673,154 @@ nsContextMenu.prototype = {
                                         .QueryInterface(Ci.nsIInterfaceRequestor)
                                         .getInterface(Ci.nsIDOMWindowUtils)
                                         .outerWindowID;
     }
     this.onSocial = !!this.browser.getAttribute("origin");
 
     // Check if we are in a synthetic document (stand alone image, video, etc.).
     this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
-    // First, do checks for nodes that never have children.
-    if (this.target.nodeType == Node.ELEMENT_NODE) {
-      // See if the user clicked on an image. This check mirrors
-      // nsDocumentViewer::GetInImage. Make sure to update both if this is
-      // changed.
-      if (this.target instanceof Ci.nsIImageLoadingContent &&
-          this.target.currentURI) {
-        this.onImage = true;
+
+    this._setTargetForNodesNoChildren(editFlags, aRangeParent, aRangeOffset);
 
-        var request =
-          this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
-        if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
-          this.onLoadedImage = true;
-        if (request &&
-            (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
-            !(request.imageStatus & request.STATUS_ERROR)) {
-          this.onCompletedImage = true;
-        }
-
-        this.mediaURL = this.target.currentURI.spec;
+    this._setTargetForNodesWithChildren(editFlags, aRangeParent, aRangeOffset);
+  },
 
-        var descURL = this.target.getAttribute("longdesc");
-        if (descURL) {
-          this.imageDescURL = makeURLAbsolute(ownerDoc.body.baseURI, descURL);
-        }
-      } else if (this.target instanceof HTMLCanvasElement) {
-        this.onCanvas = true;
-      } else if (this.target instanceof HTMLVideoElement) {
-        let mediaURL = this.target.currentSrc || this.target.src;
-        if (this.isMediaURLReusable(mediaURL)) {
-          this.mediaURL = mediaURL;
-        }
-        if (this._isProprietaryDRM()) {
-          this.onDRMMedia = true;
-        }
-        // Firefox always creates a HTMLVideoElement when loading an ogg file
-        // directly. If the media is actually audio, be smarter and provide a
-        // context menu with audio operations.
-        if (this.target.readyState >= this.target.HAVE_METADATA &&
-            (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
-          this.onAudio = true;
-        } else {
-          this.onVideo = true;
-        }
-      } else if (this.target instanceof HTMLAudioElement) {
-        this.onAudio = true;
-        let mediaURL = this.target.currentSrc || this.target.src;
-        if (this.isMediaURLReusable(mediaURL)) {
-          this.mediaURL = mediaURL;
-        }
-        if (this._isProprietaryDRM()) {
-          this.onDRMMedia = true;
-        }
-      } else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
-        this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
-        this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
-        this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
-        this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
-        if (this.onEditableArea) {
-          if (this.isRemote) {
-            InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
-          } else {
-            InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
-            InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
-          }
-        }
-        this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
-      } else if (this.target instanceof HTMLHtmlElement) {
-        var bodyElt = ownerDoc.body;
-        if (bodyElt) {
-          let computedURL;
-          try {
-            computedURL = this.getComputedURL(bodyElt, "background-image");
-            this._hasMultipleBGImages = false;
-          } catch (e) {
-            this._hasMultipleBGImages = true;
-          }
-          if (computedURL) {
-            this.hasBGImage = true;
-            this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
-                                              computedURL);
-          }
-        }
-      } else if ((this.target instanceof HTMLEmbedElement ||
-                this.target instanceof HTMLObjectElement ||
-                this.target instanceof HTMLAppletElement) &&
-               this.target.displayedType == HTMLObjectElement.TYPE_NULL &&
-               this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
-        this.onCTPPlugin = true;
-      }
-
-      this.canSpellCheck = this._isSpellCheckEnabled(this.target);
-    } else if (this.target.nodeType == Node.TEXT_NODE) {
+  /**
+   * Sets up the parts of the context menu for when when nodes have no children.
+   *
+   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
+   *                            for the details.
+   * @param {nsIDOMNode} rangeParent The parent node for where the selection ends.
+   * @param {Integer} rangeOffset The end position of where the selction ends.
+   */
+  _setTargetForNodesNoChildren(editFlags, rangeParent, rangeOffset) {
+    if (this.target.nodeType == Node.TEXT_NODE) {
       // For text nodes, look at the parent node to determine the spellcheck attribute.
       this.canSpellCheck = this.target.parentNode &&
                            this._isSpellCheckEnabled(this.target);
+      return;
     }
 
+    // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
+    // early if we don't have one.
+    if (this.target.nodeType != Node.ELEMENT_NODE) {
+      return;
+    }
+    // See if the user clicked on an image. This check mirrors
+    // nsDocumentViewer::GetInImage. Make sure to update both if this is
+    // changed.
+    if (this.target instanceof Ci.nsIImageLoadingContent &&
+        this.target.currentURI) {
+      this.onImage = true;
+
+      var request =
+        this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+      if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
+        this.onLoadedImage = true;
+      if (request &&
+          (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
+          !(request.imageStatus & request.STATUS_ERROR)) {
+        this.onCompletedImage = true;
+      }
+
+      this.mediaURL = this.target.currentURI.spec;
+
+      var descURL = this.target.getAttribute("longdesc");
+      if (descURL) {
+        this.imageDescURL = makeURLAbsolute(this.ownerDoc.body.baseURI, descURL);
+      }
+    } else if (this.target instanceof HTMLCanvasElement) {
+      this.onCanvas = true;
+    } else if (this.target instanceof HTMLVideoElement) {
+      let mediaURL = this.target.currentSrc || this.target.src;
+      if (this.isMediaURLReusable(mediaURL)) {
+        this.mediaURL = mediaURL;
+      }
+      if (this._isProprietaryDRM()) {
+        this.onDRMMedia = true;
+      }
+      // Firefox always creates a HTMLVideoElement when loading an ogg file
+      // directly. If the media is actually audio, be smarter and provide a
+      // context menu with audio operations.
+      if (this.target.readyState >= this.target.HAVE_METADATA &&
+          (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
+        this.onAudio = true;
+      } else {
+        this.onVideo = true;
+      }
+    } else if (this.target instanceof HTMLAudioElement) {
+      this.onAudio = true;
+      let mediaURL = this.target.currentSrc || this.target.src;
+      if (this.isMediaURLReusable(mediaURL)) {
+        this.mediaURL = mediaURL;
+      }
+      if (this._isProprietaryDRM()) {
+        this.onDRMMedia = true;
+      }
+    } else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
+      this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
+      this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
+      this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+      this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
+      if (this.onEditableArea) {
+        if (this.isRemote) {
+          InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+        } else {
+          InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
+          InlineSpellCheckerUI.initFromEvent(rangeParent, rangeOffset);
+        }
+      }
+      this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
+    } else if (this.target instanceof HTMLHtmlElement) {
+      var bodyElt = this.ownerDoc.body;
+      if (bodyElt) {
+        let computedURL;
+        try {
+          computedURL = this.getComputedURL(bodyElt, "background-image");
+          this._hasMultipleBGImages = false;
+        } catch (e) {
+          this._hasMultipleBGImages = true;
+        }
+        if (computedURL) {
+          this.hasBGImage = true;
+          this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
+                                            computedURL);
+        }
+      }
+    } else if ((this.target instanceof HTMLEmbedElement ||
+              this.target instanceof HTMLObjectElement ||
+              this.target instanceof HTMLAppletElement) &&
+             this.target.displayedType == HTMLObjectElement.TYPE_NULL &&
+             this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
+      this.onCTPPlugin = true;
+    }
+
+    this.canSpellCheck = this._isSpellCheckEnabled(this.target);
+  },
+
+  /**
+   * Sets up the parts of the context menu for when when nodes have children.
+   *
+   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
+   *                            for the details.
+   * @param {nsIDOMNode} rangeParent The parent node for where the selection ends.
+   * @param {Integer} rangeOffset The end position of where the selction ends.
+   */
+  _setTargetForNodesWithChildren(editFlags, rangeParent, rangeOffset) {
     // Second, bubble out, looking for items of interest that can have childen.
     // Always pick the innermost link, background image, etc.
     var elem = this.target;
     while (elem) {
       if (elem.nodeType == Node.ELEMENT_NODE) {
         // Link?
         if (!this.onLink &&
             // Be consistent with what hrefAndLinkNodeForClickEvent
             // does in browser.js
-             (isXULTextLinkLabel(elem) ||
+             (this._isXULTextLinkLabel(elem) ||
               (elem instanceof HTMLAnchorElement && elem.href) ||
               (elem instanceof HTMLAreaElement && elem.href) ||
               elem instanceof HTMLLinkElement ||
               elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) {
 
           // Target is a link or a descendant of a link.
           this.onLink = true;
 
@@ -829,21 +868,21 @@ nsContextMenu.prototype = {
     // See if the user clicked on MathML
     const NS_MathML = "http://www.w3.org/1998/Math/MathML";
     if ((this.target.nodeType == Node.TEXT_NODE &&
          this.target.parentNode.namespaceURI == NS_MathML)
          || (this.target.namespaceURI == NS_MathML))
       this.onMathML = true;
 
     // See if the user clicked in a frame.
-    var docDefaultView = ownerDoc.defaultView;
+    var docDefaultView = this.ownerDoc.defaultView;
     if (docDefaultView != docDefaultView.top) {
       this.inFrame = true;
 
-      if (ownerDoc.isSrcdocDocument) {
+      if (this.ownerDoc.isSrcdocDocument) {
           this.inSrcdocFrame = true;
       }
     }
 
     // if the document is editable, show context menu like in text inputs
     if (!this.onEditableArea) {
       if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
         // If this.onEditableArea is false but editFlags is CONTENTEDITABLE, then
@@ -857,36 +896,43 @@ nsContextMenu.prototype = {
         this.inFrame           = false;
         this.inSrcdocFrame     = false;
         this.hasBGImage        = false;
         this.isDesignMode      = true;
         this.onEditableArea = true;
         if (this.isRemote) {
           InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
         } else {
-          var targetWin = ownerDoc.defaultView;
+          var targetWin = this.ownerDoc.defaultView;
           var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
                                         .getInterface(Ci.nsIWebNavigation)
                                         .QueryInterface(Ci.nsIInterfaceRequestor)
                                         .getInterface(Ci.nsIEditingSession);
           InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
-          InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+          InlineSpellCheckerUI.initFromEvent(rangeParent, rangeOffset);
         }
         var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
         this.showItem("spell-check-enabled", canSpell);
         this.showItem("spell-separator", canSpell);
       }
     }
+  },
 
-    function isXULTextLinkLabel(node) {
-      return node.namespaceURI == xulNS &&
-             node.tagName == "label" &&
-             node.classList.contains("text-link") &&
-             node.href;
-    }
+  /**
+   * Determines if a node is a XUL Text link.
+   *
+   * @param {Object} node The object to test.
+   * @returns {Boolean} true if the object is a XUL text link.
+   */
+  _isXULTextLinkLabel(node) {
+    const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    return node.namespaceURI == xulNS &&
+           node.tagName == "label" &&
+           node.classList.contains("text-link") &&
+           node.href;
   },
 
   // Returns the computed style attribute for the given element.
   getComputedStyle(aElem, aProp) {
     return aElem.ownerGlobal
                 .getComputedStyle(aElem).getPropertyValue(aProp);
   },