Bug 978918 - Filter hidden <br> when get content editable text length. r=yxl, a=1.4+
authorWei Deng <wdeng@mozilla.com>
Thu, 20 Mar 2014 18:37:00 +0800
changeset 192200 026e58a7a2415b62eef7a881b4a83409bf40c8d3
parent 192199 2ca42934ab2179c8afccd561af2bf4f857c6aa1d
child 192201 e84a745fbeec89535c7542f1590839afe4425c8b
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyxl, 1
bugs978918
milestone30.0a2
Bug 978918 - Filter hidden <br> when get content editable text length. r=yxl, a=1.4+
dom/inputmethod/Keyboard.jsm
dom/inputmethod/forms.js
dom/inputmethod/mochitest/file_test_sms_app.html
dom/inputmethod/mochitest/mochitest.ini
dom/inputmethod/mochitest/test_bug978918.html
--- a/dom/inputmethod/Keyboard.jsm
+++ b/dom/inputmethod/Keyboard.jsm
@@ -80,17 +80,19 @@ this.Keyboard = {
   },
 
   initFormsFrameScript: function(mm) {
     mm.addMessageListener('Forms:Input', this);
     mm.addMessageListener('Forms:SelectionChange', this);
     mm.addMessageListener('Forms:GetText:Result:OK', this);
     mm.addMessageListener('Forms:GetText:Result:Error', this);
     mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
+    mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this);
     mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
+    mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this);
     mm.addMessageListener('Forms:SendKey:Result:OK', this);
     mm.addMessageListener('Forms:SendKey:Result:Error', this);
     mm.addMessageListener('Forms:SequenceError', this);
     mm.addMessageListener('Forms:GetContext:Result:OK', this);
     mm.addMessageListener('Forms:SetComposition:Result:OK', this);
     mm.addMessageListener('Forms:EndComposition:Result:OK', this);
   },
 
@@ -141,16 +143,18 @@ this.Keyboard = {
       case 'Forms:SetSelectionRange:Result:OK':
       case 'Forms:ReplaceSurroundingText:Result:OK':
       case 'Forms:SendKey:Result:OK':
       case 'Forms:SendKey:Result:Error':
       case 'Forms:SequenceError':
       case 'Forms:GetContext:Result:OK':
       case 'Forms:SetComposition:Result:OK':
       case 'Forms:EndComposition:Result:OK':
+      case 'Forms:SetSelectionRange:Result:Error':
+      case 'Forms:ReplaceSurroundingText:Result:Error':
         let name = msg.name.replace(/^Forms/, 'Keyboard');
         this.forwardEvent(name, msg);
         break;
 
       case 'Keyboard:SetValue':
         this.setValue(msg);
         break;
       case 'Keyboard:RemoveFocus':
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -19,16 +19,22 @@ XPCOMUtils.defineLazyServiceGetter(Servi
                                    "nsIFocusManager");
 
 XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () {
   return content.QueryInterface(Ci.nsIInterfaceRequestor)
                 .getInterface(Ci.nsIDOMWindowUtils);
 });
 
 const RESIZE_SCROLL_DELAY = 20;
+// In content editable node, when there are hidden elements such as <br>, it
+// may need more than one (usually less than 3 times) move/extend operations
+// to change the selection range. If we cannot change the selection range
+// with more than 20 opertations, we are likely being blocked and cannot change
+// the selection range any more.
+const MAX_BLOCKED_COUNT = 20;
 
 let HTMLDocument = Ci.nsIDOMHTMLDocument;
 let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
 let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
@@ -565,38 +571,56 @@ let FormAssistant = {
         break;
       }
 
       case "Forms:SetSelectionRange":  {
         CompositionManager.endComposition('');
 
         let start = json.selectionStart;
         let end =  json.selectionEnd;
-        setSelectionRange(target, start, end);
+
+        if (!setSelectionRange(target, start, end)) {
+          if (json.requestId) {
+            sendAsyncMessage("Forms:SetSelectionRange:Result:Error", {
+              requestId: json.requestId,
+              error: "failed"
+            });
+          }
+          break;
+        }
+
         this.updateSelection();
 
         if (json.requestId) {
           sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
             requestId: json.requestId,
             selectioninfo: this.getSelectionInfo()
           });
         }
         break;
       }
 
       case "Forms:ReplaceSurroundingText": {
         CompositionManager.endComposition('');
 
         let selectionRange = getSelectionRange(target);
-        replaceSurroundingText(target,
-                               json.text,
-                               selectionRange[0],
-                               selectionRange[1],
-                               json.offset,
-                               json.length);
+        if (!replaceSurroundingText(target,
+                                    json.text,
+                                    selectionRange[0],
+                                    selectionRange[1],
+                                    json.offset,
+                                    json.length)) {
+          if (json.requestId) {
+            sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", {
+              requestId: json.requestId,
+              error: "failed"
+            });
+          }
+          break;
+        }
 
         if (json.requestId) {
           sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
             requestId: json.requestId,
             selectioninfo: this.getSelectionInfo()
           });
         }
         break;
@@ -906,16 +930,17 @@ function getListForElement(element) {
 };
 
 // Create a plain text document encode from the focused element.
 function getDocumentEncoder(element) {
   let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
                 .createInstance(Ci.nsIDocumentEncoder);
   let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent |
               Ci.nsIDocumentEncoder.OutputRaw |
+              Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
               // Bug 902847. Don't trim trailing spaces of a line.
               Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces |
               Ci.nsIDocumentEncoder.OutputLFLineBreak |
               Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder;
   encoder.init(element.ownerDocument, "text/plain", flags);
   return encoder;
 }
 
@@ -973,17 +998,17 @@ function getContentEditableSelectionLeng
 function setSelectionRange(element, start, end) {
   let isTextField = isPlainTextField(element);
 
   // Check the parameters
 
   if (!isTextField && !isContentEditable(element)) {
     // Skip HTMLOptionElement and HTMLSelectElement elements, as they don't
     // support the operation of setSelectionRange
-    return;
+    return false;
   }
 
   let text = isTextField ? element.value : getContentEditableText(element);
   let length = text.length;
   if (start < 0) {
     start = 0;
   }
   if (end > length) {
@@ -991,40 +1016,70 @@ function setSelectionRange(element, star
   }
   if (start > end) {
     start = end;
   }
 
   if (isTextField) {
     // Set the selection range of <input> and <textarea> elements
     element.setSelectionRange(start, end, "forward");
+    return true;
   } else {
     // set the selection range of contenteditable elements
     let win = element.ownerDocument.defaultView;
     let sel = win.getSelection();
 
     // Move the caret to the start position
     sel.collapse(element, 0);
     for (let i = 0; i < start; i++) {
       sel.modify("move", "forward", "character");
     }
 
-    while (getContentEditableSelectionStart(element, sel) < start) {
+    // Avoid entering infinite loop in case we cannot change the selection
+    // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
+    let oldStart = getContentEditableSelectionStart(element, sel);
+    let counter = 0;
+    while (oldStart < start) {
       sel.modify("move", "forward", "character");
+      let newStart = getContentEditableSelectionStart(element, sel);
+      if (oldStart == newStart) {
+        counter++;
+        if (counter > MAX_BLOCKED_COUNT) {
+          return false;
+        }
+      } else {
+        counter = 0;
+        oldStart = newStart;
+      }
     }
 
     // Extend the selection to the end position
     for (let i = start; i < end; i++) {
       sel.modify("extend", "forward", "character");
     }
 
+    // Avoid entering infinite loop in case we cannot change the selection
+    // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
+    counter = 0;
     let selectionLength = end - start;
-    while (getContentEditableSelectionLength(element, sel) < selectionLength) {
+    let oldSelectionLength = getContentEditableSelectionLength(element, sel);
+    while (oldSelectionLength  < selectionLength) {
       sel.modify("extend", "forward", "character");
+      let newSelectionLength = getContentEditableSelectionLength(element, sel);
+      if (oldSelectionLength == newSelectionLength ) {
+        counter++;
+        if (counter > MAX_BLOCKED_COUNT) {
+          return false;
+        }
+      } else {
+        counter = 0;
+        oldSelectionLength = newSelectionLength;
+      }
     }
+    return true;
   }
 }
 
 /**
  * Scroll the given element into view.
  *
  * Calls scrollSelectionIntoView for contentEditable elements.
  */
@@ -1063,46 +1118,49 @@ function getPlaintextEditor(element) {
   }
   return editor;
 }
 
 function replaceSurroundingText(element, text, selectionStart, selectionEnd,
                                 offset, length) {
   let editor = FormAssistant.editor;
   if (!editor) {
-    return;
+    return false;
   }
 
   // Check the parameters.
   let start = selectionStart + offset;
   if (start < 0) {
     start = 0;
   }
   if (length < 0) {
     length = 0;
   }
   let end = start + length;
 
   if (selectionStart != start || selectionEnd != end) {
     // Change selection range before replacing.
-    setSelectionRange(element, start, end);
+    if (!setSelectionRange(element, start, end)) {
+      return false;
+    }
   }
 
   if (start != end) {
     // Delete the selected text.
     editor.deleteSelection(Ci.nsIEditor.ePrevious, Ci.nsIEditor.eStrip);
   }
 
   if (text) {
     // We don't use CR but LF
     // see https://bugzilla.mozilla.org/show_bug.cgi?id=902847
     text = text.replace(/\r/g, '\n');
     // Insert the text to be replaced with.
     editor.insertText(text);
   }
+  return true;
 }
 
 let CompositionManager =  {
   _isStarted: false,
   _text: '',
   _clauseAttrMap: {
     'raw-input':
       Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT,
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_sms_app.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+  <div id="messages-input" x-inputmode="-moz-sms" contenteditable="true"
+    autofocus="autofocus">Httvb<br></div>
+  <script type="application/javascript;version=1.7">
+    let input = document.getElementById('messages-input');
+    input.focus();
+  </script>
+</body>
+</html>
+ </div>
+</body>
+</html>
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -1,13 +1,15 @@
 [DEFAULT]
 skip-if = toolkit == 'android' || e10s #Not supported on Android
 support-files =
   inputmethod_common.js
   file_inputmethod.html
   file_test_app.html
   file_test_sendkey_cancel.html
+  file_test_sms_app.html
 
 [test_basic.html]
 [test_bug944397.html]
 [test_bug949059.html]
+[test_bug978918.html]
+[test_delete_focused_element.html]
 [test_sendkey_cancel.html]
-[test_delete_focused_element.html]
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug978918.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=978918
+-->
+<head>
+  <title>Basic test for InputMethod API.</title>
+  <script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=978918">Mozilla Bug 978918</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+// The input context.
+var gContext = null;
+
+inputmethod_setup(function() {
+  runTest();
+});
+
+function runTest() {
+  let im = navigator.mozInputMethod;
+
+  im.oninputcontextchange = function() {
+    ok(true, 'inputcontextchange event was fired.');
+    im.oninputcontextchange = null;
+
+    gContext = im.inputcontext;
+    if (!gContext) {
+      ok(false, 'Should have a non-null inputcontext.');
+      inputmethod_cleanup();
+      return;
+    }
+
+    test_setSelectionRange();
+  };
+
+  // Set current page as an input method.
+  SpecialPowers.wrap(im).setActive(true);
+
+  let iframe = document.createElement('iframe');
+  iframe.src = 'file_test_sms_app.html';
+  iframe.setAttribute('mozbrowser', true);
+  document.body.appendChild(iframe);
+}
+
+function test_setSelectionRange() {
+  gContext.setSelectionRange(0, 100).then(function() {
+    is(gContext.selectionStart, 0, 'selectionStart was set successfully.');
+    is(gContext.selectionEnd, 5, 'selectionEnd was set successfully.');
+    test_replaceSurroundingText();
+  }, function(e) {
+    ok(false, 'setSelectionRange failed:' + e.name);
+    inputmethod_cleanup();
+  });
+}
+
+function test_replaceSurroundingText() {
+  // Replace 'Httvb' with 'Hito'.
+  gContext.replaceSurroundingText('Hito', 0, 100).then(function() {
+    ok(true, 'replaceSurroundingText finished');
+    inputmethod_cleanup();
+  }, function(e) {
+    ok(false, 'replaceSurroundingText failed: ' + e.name);
+    inputmethod_cleanup();
+  });
+}
+
+</script>
+</pre>
+</body>
+</html>
+