Bug 905176 - Don't show the Check Spelling menu item for spellcheck=false contenteditable elements; r=mconley a=bajaj
authorEhsan Akhgari <ehsan@mozilla.com>
Tue, 20 Aug 2013 10:52:13 -0400
changeset 148396 b8c61de105bf257664f8568901f05ded9670d163
parent 148395 148161ac177f62c2895db87704421201ed1fdad5
child 148397 0cb473e881009fed93228934b305a54683079a25
push id2783
push usereakhgari@mozilla.com
push dateTue, 20 Aug 2013 14:56:01 +0000
treeherdermozilla-beta@b8c61de105bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, bajaj
bugs905176
milestone24.0
Bug 905176 - Don't show the Check Spelling menu item for spellcheck=false contenteditable elements; r=mconley a=bajaj
browser/base/content/nsContextMenu.js
browser/base/content/test/subtst_contextmenu.html
browser/base/content/test/test_contextmenu.html
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -329,17 +329,17 @@ nsContextMenu.prototype = {
     this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
     this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
     this.showItem("context-shareimage", shareEnabled && this.onImage);
     this.showItem("context-sharevideo", shareEnabled && this.onVideo);
     this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL);
   },
 
   initSpellingItems: function() {
-    var canSpell = InlineSpellCheckerUI.canSpellCheck;
+    var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
     var onMisspelling = InlineSpellCheckerUI.overMisspelling;
     var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
     this.showItem("spell-check-enabled", canSpell);
     this.showItem("spell-separator", canSpell || this.onEditableArea);
     document.getElementById("spell-check-enabled")
             .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
 
     this.showItem("spell-add-to-dictionary", onMisspelling);
@@ -354,17 +354,17 @@ nsContextMenu.prototype = {
         InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode,
                                                   suggestionsSeparator, 5);
       this.showItem("spell-no-suggestions", numsug == 0);
     }
     else
       this.showItem("spell-no-suggestions", false);
 
     // dictionary list
-    this.showItem("spell-dictionaries", InlineSpellCheckerUI.enabled);
+    this.showItem("spell-dictionaries", canSpell && InlineSpellCheckerUI.enabled);
     if (canSpell) {
       var dictMenu = document.getElementById("spell-dictionaries-menu");
       var dictSep = document.getElementById("spell-language-separator");
       InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
       this.showItem("spell-add-dictionaries-main", false);
     }
     else if (this.onEditableArea) {
       // when there is no spellchecker but we might be able to spellcheck
@@ -515,16 +515,17 @@ nsContextMenu.prototype = {
     this.onMathML          = false;
     this.inFrame           = false;
     this.inSyntheticDoc    = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
     this.onEditableArea    = false;
     this.isDesignMode      = false;
     this.onCTPPlugin       = false;
+    this.canSpellCheck     = false;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
     this.browser = this.target.ownerDocument.defaultView
                                 .QueryInterface(Ci.nsIInterfaceRequestor)
                                 .getInterface(Ci.nsIWebNavigation)
                                 .QueryInterface(Ci.nsIDocShell)
@@ -605,16 +606,23 @@ nsContextMenu.prototype = {
         }
       }
       else if ((this.target instanceof HTMLEmbedElement ||
                 this.target instanceof HTMLObjectElement ||
                 this.target instanceof HTMLAppletElement) &&
                this.target.mozMatchesSelector(":-moz-handler-clicktoplay")) {
         this.onCTPPlugin = true;
       }
+
+      this.canSpellCheck = this._isSpellCheckEnabled(this.target);
+    }
+    else 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);
     }
 
     // Second, bubble out, looking for items of interest that can have childen.
     // Always pick the innermost link, background image, etc.
     const XMLNS = "http://www.w3.org/XML/1998/namespace";
     var elem = this.target;
     while (elem) {
       if (elem.nodeType == Node.ELEMENT_NODE) {
@@ -700,17 +708,17 @@ nsContextMenu.prototype = {
           this.onLoadedImage     = false;
           this.onCompletedImage  = false;
           this.onMathML          = false;
           this.inFrame           = false;
           this.hasBGImage        = false;
           this.isDesignMode      = true;
           this.onEditableArea = true;
           InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win));
-          var canSpell = InlineSpellCheckerUI.canSpellCheck;
+          var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
           InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
           this.showItem("spell-check-enabled", canSpell);
           this.showItem("spell-separator", canSpell);
         }
       }
     }
   },
 
@@ -741,16 +749,33 @@ nsContextMenu.prototype = {
     // until we do.
     return this.linkProtocol && !(
              this.linkProtocol == "mailto"     ||
              this.linkProtocol == "javascript" ||
              this.linkProtocol == "news"       ||
              this.linkProtocol == "snews"      );
   },
 
+  _isSpellCheckEnabled: function(aNode) {
+    // We can always force-enable spellchecking on textboxes
+    if (this.isTargetATextBox(aNode)) {
+      return true;
+    }
+    // We can never spell check something which is not content editable
+    var editable = aNode.isContentEditable;
+    if (!editable && aNode.ownerDocument) {
+      editable = aNode.ownerDocument.designMode == "on";
+    }
+    if (!editable) {
+      return false;
+    }
+    // Otherwise make sure that nothing in the parent chain disables spellchecking
+    return aNode.spellcheck;
+  },
+
   // Open linked-to URL in a new window.
   openLink : function () {
     var doc = this.target.ownerDocument;
     urlSecurityCheck(this.linkURL, doc.nodePrincipal);
     openLinkIn(this.linkURL, "window",
                { charset: doc.characterSet,
                  referrerURI: doc.documentURIObject });
   },
--- a/browser/base/content/test/subtst_contextmenu.html
+++ b/browser/base/content/test/subtst_contextmenu.html
@@ -19,16 +19,17 @@ Browser context menu subtest.
   <source src="bogus.duh" type="video/durrrr;">
 </video>
 <iframe id="test-iframe" width="98"  height="98" style="border: 1px solid black"></iframe>
 <iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
 <iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe>
 <textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion -->
 <div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions -->
 <input id="test-input-spellcheck" type="text" spellcheck="true" autofocus value="prodkjfgigrty"> <!-- this one also generates one suggestion -->
+<div id="test-contenteditable-spellcheck-false" contenteditable="true" spellcheck="false">test</div> <!-- No Check Spelling menu item -->
 <div id="test-dom-full-screen">DOM full screen FTW</div>
 <div contextmenu="myMenu">
   <p id="test-pagemenu" hopeless="true">I've got a context menu!</p>
   <menu id="myMenu" type="context">
     <menuitem label="Plain item" onclick="document.getElementById('test-pagemenu').removeAttribute('hopeless');"></menuitem>
     <menuitem label="Disabled item" disabled></menuitem>
     <menuitem> Item w/ textContent</menuitem>
     <menu>
--- a/browser/base/content/test/test_contextmenu.html
+++ b/browser/base/content/test/test_contextmenu.html
@@ -117,17 +117,18 @@ function getVisibleMenuItems(aMenu, aDat
             var label = item.getAttribute("label");
             ok(label.length, "menuitem " + item.id + " has a label");
             if (isSpellSuggestion) {
               is(key, "", "Spell suggestions shouldn't have an access key");
               items.push("*" + label);
             } else if (isGenerated) {
               items.push("+" + label);
             } else if (item.id.indexOf("spell-check-dictionary-") != 0 &&
-                       item.id != "spell-no-suggestions") {
+                       item.id != "spell-no-suggestions" &&
+                       item.id != "spell-add-dictionaries-main") {
               ok(key, "menuitem " + item.id + " has an access key");
               if (accessKeys[key])
                   ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]);
               else
                   accessKeys[key] = item.id;
             }
             if (!isSpellSuggestion && !isGenerated) {
               items.push(item.id);
@@ -655,26 +656,44 @@ function runTest(testNum) {
                           "spell-check-enabled", true,
                           "spell-dictionaries",  true,
                               ["spell-check-dictionary-en-US", true,
                                "---",                          null,
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
 
         closeContextMenu();
+        openContextMenuFor(inputspellfalse, false, true); // Invoke context menu for next test.
+        break;
+
+    case 20:
+        // Context menu for text input field with spellcheck=false
+        checkContextMenu(["context-undo",        false,
+                          "---",                 null,
+                          "context-cut",         false,
+                          "context-copy",        false,
+                          "context-paste",       null, // ignore clipboard state
+                          "context-delete",      false,
+                          "---",                 null,
+                          "context-selectall",   true,
+                          "---",                 null,
+                          "spell-add-dictionaries-main",  true,
+                         ].concat(inspectItems));
+
+        closeContextMenu();
         openContextMenuFor(link); // Invoke context menu for next test.
         break;
 
-    case 20:
+    case 21:
         executeCopyCommand("cmd_copyLink", "http://mozilla.com/");
         closeContextMenu();
         openContextMenuFor(pagemenu); // Invoke context menu for next test.
         break;
 
-    case 21:
+    case 22:
         // Context menu for element with assigned content context menu
         checkContextMenu(["+Plain item",          {type: "", icon: "", checked: false, disabled: false},
                           "+Disabled item",       {type: "", icon: "", checked: false, disabled: true},
                           "+Item w/ textContent", {type: "", icon: "", checked: false, disabled: false},
                           "---",                  null,
                           "+Checkbox",            {type: "checkbox", icon: "", checked: true, disabled: false},
                           "---",                  null,
                           "+Radio1",              {type: "checkbox", icon: "", checked: true, disabled: false},
@@ -715,17 +734,17 @@ function runTest(testNum) {
             openContextMenuFor(dom_full_screen, true); // Invoke context menu for next test.
         }
         subwindow.addEventListener("mozfullscreenchange", openDomFullScreen, false);
         SpecialPowers.setBoolPref("full-screen-api.approval-required", false);
         SpecialPowers.setBoolPref("full-screen-api.allow-trusted-requests-only", false);
         full_screen_element.mozRequestFullScreen();
         break;
 
-        case 22:
+    case 23:
         // Context menu for DOM Fullscreen mode (NOTE: this is *NOT* on an img)
         checkContextMenu(["context-leave-dom-fullscreen", true,
                           "---",                          null,
                           "context-back",                 false,
                           "context-forward",              false,
                           "context-reload",               true,
                           "---",                          null,
                           "context-bookmarkpage",         true,
@@ -744,17 +763,17 @@ function runTest(testNum) {
             SpecialPowers.clearUserPref("full-screen-api.approval-required");
             SpecialPowers.clearUserPref("full-screen-api.allow-trusted-requests-only");
             openContextMenuFor(pagemenu, true); // Invoke context menu for next test.
         }
         subwindow.addEventListener("mozfullscreenchange", openPagemenu, false);
         subwindow.document.mozCancelFullScreen();
         break;
 
-    case 23:
+    case 24:
         // Context menu for element with assigned content context menu
         // The shift key should bypass content context menu processing
         checkContextMenu(["context-back",         false,
                           "context-forward",      false,
                           "context-reload",       true,
                           "---",                  null,
                           "context-bookmarkpage", true,
                           "context-savepage",     true,
@@ -765,33 +784,33 @@ function runTest(testNum) {
                           "context-viewsource",   true,
                           "context-viewinfo",     true
                          ].concat(inspectItems));
         closeContextMenu();
         selectText(selecttext); // Select text prior to opening context menu.
         openContextMenuFor(selecttext); // Invoke context menu for next test.
         return;
 
-    case 24:
+    case 25:
         // Context menu for selected text
         if (SpecialPowers.Services.appinfo.OS == "Darwin") {
           // This test is only enabled on Mac due to bug 736399.
           checkContextMenu(["context-copy",                        true,
                             "context-selectall",                   true,
                             "---",                                 null,
                             "context-searchselect",                true,
                             "context-viewpartialsource-selection", true
                            ].concat(inspectItems));
         }
         closeContextMenu();
         selectText(selecttextlink); // Select text prior to opening context menu.
         openContextMenuFor(selecttextlink); // Invoke context menu for next test.
         return;
 
-    case 25:
+    case 26:
         // Context menu for selected text which matches valid URL pattern
         if (SpecialPowers.Services.appinfo.OS == "Darwin") {
           // This test is only enabled on Mac due to bug 736399.
           if (perWindowPrivateBrowsing) {
             checkContextMenu(["context-openlinkincurrent",           true,
                               "context-openlinkintab",               true,
                               "context-openlink",                    true,
                               "context-openlinkprivate",             true,
@@ -821,17 +840,17 @@ function runTest(testNum) {
         }
         closeContextMenu();
         // clear the selection because following tests don't expect any selection
         subwindow.getSelection().removeAllRanges();
 
         openContextMenuFor(imagelink)
         break;
 
-    case 26:
+    case 27:
         // Context menu for image link
         if (perWindowPrivateBrowsing) {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "context-openlinkprivate", true,
                             "---",                   null,
                             "context-bookmarklink",  true,
                             "context-savelink",      true,
@@ -864,17 +883,17 @@ function runTest(testNum) {
                             "context-viewimageinfo",        true
                            ].concat(inspectItems));
         }
         closeContextMenu();
         selectInputText(select_inputtext); // Select text prior to opening context menu.
         openContextMenuFor(select_inputtext); // Invoke context menu for next test.
         return;
 
-    case 27:
+    case 28:
         // Context menu for selected text in input
         checkContextMenu(["context-undo",        false,
                           "---",                 null,
                           "context-cut",         true,
                           "context-copy",        true,
                           "context-paste",       null, // ignore clipboard state
                           "context-delete",      true,
                           "---",                 null,
@@ -883,17 +902,17 @@ function runTest(testNum) {
                           "---",                 null,
                           "spell-check-enabled", true
                          ].concat(inspectItems));
         closeContextMenu();
         selectInputText(select_inputtext_password); // Select text prior to opening context menu.
         openContextMenuFor(select_inputtext_password); // Invoke context menu for next test.
         return;
 
-    case 28:
+    case 29:
         // Context menu for selected text in input[type="password"]
         checkContextMenu(["context-undo",        false,
                           "---",                 null,
                           "context-cut",         true,
                           "context-copy",        true,
                           "context-paste",       null, // ignore clipboard state
                           "context-delete",      true,
                           "---",                 null,
@@ -906,17 +925,17 @@ function runTest(testNum) {
                                "---",                          null,
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
         closeContextMenu();
         subwindow.getSelection().removeAllRanges();
         openContextMenuFor(plugin);
         return;
 
-    case 29:
+    case 30:
         // Context menu for click-to-play blocked plugin
         checkContextMenu(["context-ctp-play",     true,
                           "context-ctp-hide",     true,
                           "---",                  null,
                           "context-back",         false,
                           "context-forward",      false,
                           "context-reload",       true,
                           "---",                  null,
@@ -963,17 +982,17 @@ function runTest(testNum) {
 
 
 var testNum = 1;
 var subwindow, chromeWin, contextMenu, lastElement;
 var text, link, mailto, input, img, canvas, video_ok, video_bad, video_bad2,
     iframe, video_in_iframe, image_in_iframe, textarea, contenteditable,
     inputspell, pagemenu, dom_full_screen, plainTextItems, audio_in_video,
     selecttext, selecttextlink, imagelink, select_inputtext, select_inputtext_password,
-    plugin;
+    plugin, inputspellfalse;
 
 function startTest() {
     chromeWin = SpecialPowers.wrap(subwindow)
                     .QueryInterface(Ci.nsIInterfaceRequestor)
                     .getInterface(Ci.nsIWebNavigation)
                     .QueryInterface(Ci.nsIDocShellTreeItem)
                     .rootTreeItem
                     .QueryInterface(Ci.nsIInterfaceRequestor)
@@ -1005,16 +1024,17 @@ function startTest() {
     iframe = subwindow.document.getElementById("test-iframe");
     video_in_iframe = subwindow.document.getElementById("test-video-in-iframe").contentDocument.getElementsByTagName("video")[0];
     video_in_iframe.pause();
     image_in_iframe = subwindow.document.getElementById("test-image-in-iframe").contentDocument.getElementsByTagName("img")[0];
     textarea = subwindow.document.getElementById("test-textarea");
     contenteditable = subwindow.document.getElementById("test-contenteditable");
     contenteditable.focus(); // content editable needs to be focused to enable spellcheck
     inputspell = subwindow.document.getElementById("test-input-spellcheck");
+    inputspellfalse = subwindow.document.getElementById("test-contenteditable-spellcheck-false");
     pagemenu = subwindow.document.getElementById("test-pagemenu");
     dom_full_screen = subwindow.document.getElementById("test-dom-full-screen");
     selecttext = subwindow.document.getElementById("test-select-text");
     selecttextlink = subwindow.document.getElementById("test-select-text-link");
     select_inputtext = subwindow.document.getElementById("test-select-input-text");
     select_inputtext_password = subwindow.document.getElementById("test-select-input-text-type-password");
     plugin = subwindow.document.getElementById("test-plugin");