Bug 1059163 - Add a mutation observer to contenteditable elements to detect selection changes that nsISelectionPrivate misses. r=yxl
authorJan Jongboom <janjongboom@gmail.com>
Thu, 09 Oct 2014 06:06:00 -0400
changeset 234548 bbcaa290276118d0bd3e75613058510ae3eb19f4
parent 234547 51c57f6ffd344930333861fa623a023c4e5794ec
child 234549 120d00383f290a312bbeace22050f54de1f58010
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyxl
bugs1059163
milestone35.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 1059163 - Add a mutation observer to contenteditable elements to detect selection changes that nsISelectionPrivate misses. r=yxl
dom/inputmethod/forms.js
dom/inputmethod/mochitest/file_test_contenteditable.html
dom/inputmethod/mochitest/mochitest.ini
dom/inputmethod/mochitest/test_bug1059163.html
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -219,17 +219,18 @@ let FormAssistant = {
   isKeyboardOpened: false,
   selectionStart: -1,
   selectionEnd: -1,
   textBeforeCursor: "",
   textAfterCursor: "",
   scrollIntoViewTimeout: null,
   _focusedElement: null,
   _focusCounter: 0, // up one for every time we focus a new element
-  _observer: null,
+  _focusDeleteObserver: null,
+  _focusContentObserver: null,
   _documentEncoder: null,
   _editor: null,
   _editing: false,
   _selectionPrivate: null,
 
   get focusedElement() {
     if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
       this._focusedElement = null;
@@ -245,19 +246,23 @@ let FormAssistant = {
   setFocusedElement: function fa_setFocusedElement(element) {
     let self = this;
 
     if (element === this.focusedElement)
       return;
 
     if (this.focusedElement) {
       this.focusedElement.removeEventListener('compositionend', this);
-      if (this._observer) {
-        this._observer.disconnect();
-        this._observer = null;
+      if (this._focusDeleteObserver) {
+        this._focusDeleteObserver.disconnect();
+        this._focusDeleteObserver = null;
+      }
+      if (this._focusContentObserver) {
+        this._focusContentObserver.disconnect();
+        this._focusContentObserver = null;
       }
       if (this._selectionPrivate) {
         this._selectionPrivate.removeSelectionListener(this);
         this._selectionPrivate = null;
       }
     }
 
     this._documentEncoder = null;
@@ -287,33 +292,46 @@ let FormAssistant = {
         if (selection) {
           this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate);
           this._selectionPrivate.addSelectionListener(this);
         }
       }
 
       // If our focusedElement is removed from DOM we want to handle it properly
       let MutationObserver = element.ownerDocument.defaultView.MutationObserver;
-      this._observer = new MutationObserver(function(mutations) {
+      this._focusDeleteObserver = new MutationObserver(function(mutations) {
         var del = [].some.call(mutations, function(m) {
           return [].some.call(m.removedNodes, function(n) {
             return n.contains(element);
           });
         });
         if (del && element === self.focusedElement) {
           self.hideKeyboard();
           self.selectionStart = -1;
           self.selectionEnd = -1;
         }
       });
 
-      this._observer.observe(element.ownerDocument.body, {
+      this._focusDeleteObserver.observe(element.ownerDocument.body, {
         childList: true,
         subtree: true
       });
+
+      // If contenteditable, also add a mutation observer on its content and
+      // call selectionChanged when a change occurs
+      if (isContentEditable(element)) {
+        this._focusContentObserver = new MutationObserver(function() {
+          this.updateSelection();
+        }.bind(this));
+
+        this._focusContentObserver.observe(element, {
+          childList: true,
+          subtree: true
+        });
+      }
     }
 
     this.focusedElement = element;
   },
 
   notifySelectionChanged: function(aDocument, aSelection, aReason) {
     this.updateSelection();
   },
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_contenteditable.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<div id="text" contenteditable>Jan Jongboom</div>
+<script type="application/javascript;version=1.7">
+  var t = document.querySelector('#text');
+
+  t.focus();
+  var range = document.createRange();
+  range.selectNodeContents(t);
+  range.collapse(false);
+  var selection = window.getSelection();
+  selection.removeAllRanges();
+  selection.addRange(range);
+</script>
+</body>
+</html>
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -1,24 +1,26 @@
 [DEFAULT]
 # Not supported on Android, bug 983015 for B2G emulator
 skip-if = (toolkit == 'android' || toolkit == 'gonk') || e10s
 support-files =
   inputmethod_common.js
   file_inputmethod.html
   file_inputmethod_1043828.html
   file_test_app.html
+  file_test_contenteditable.html
   file_test_sendkey_cancel.html
   file_test_sms_app.html
   file_test_sms_app_1066515.html
 
 [test_basic.html]
 [test_bug944397.html]
 [test_bug949059.html]
 [test_bug953044.html]
 [test_bug960946.html]
 [test_bug978918.html]
 [test_bug1026997.html]
 [test_bug1043828.html]
+[test_bug1059163.html]
 [test_bug1066515.html]
 [test_delete_focused_element.html]
 [test_sendkey_cancel.html]
 [test_two_inputs.html]
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1059163.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1059163
+-->
+<head>
+  <title>Basic test for repeat sendKey events</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=1059163">Mozilla Bug 1059163</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+inputmethod_setup(function() {
+  runTest();
+});
+
+// The frame script running in the file
+function appFrameScript() {
+  addMessageListener('test:InputMethod:clear', function() {
+    var t = content.document.getElementById('text');
+    t.innerHTML = '';
+  });
+}
+
+function runTest() {
+  let im = navigator.mozInputMethod;
+
+  // Set current page as an input method.
+  SpecialPowers.wrap(im).setActive(true);
+
+  // Create an app frame to recieve keyboard inputs.
+  let app = document.createElement('iframe');
+  app.src = 'file_test_contenteditable.html';
+  app.setAttribute('mozbrowser', true);
+  document.body.appendChild(app);
+  app.addEventListener('mozbrowserloadend', function() {
+    let mm = SpecialPowers.getBrowserFrameMessageManager(app);
+
+    function register() {
+      im.inputcontext.onselectionchange = function() {
+        im.inputcontext.onselectionchange = null;
+
+        is(im.inputcontext.textBeforeCursor, '', 'textBeforeCursor');
+        is(im.inputcontext.textBeforeCursor, '', 'textAfterCursor');
+        is(im.inputcontext.selectionStart, 0, 'selectionStart');
+        is(im.inputcontext.selectionEnd, 0, 'selectionEnd');
+
+        inputmethod_cleanup();
+      };
+
+      mm.sendAsyncMessage('test:InputMethod:clear');
+    }
+
+    if (im.inputcontext) {
+       register();
+    }
+    else {
+      im.oninputcontextchange = function() {
+        if (im.inputcontext) {
+          im.oninputcontextchange = null;
+          register();
+        }
+      };
+    }
+
+    mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
+  });
+}
+</script>
+</pre>
+</body>
+</html>
+