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 210013 bbcaa290276118d0bd3e75613058510ae3eb19f4
parent 210012 51c57f6ffd344930333861fa623a023c4e5794ec
child 210014 120d00383f290a312bbeace22050f54de1f58010
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersyxl
bugs1059163
milestone35.0a1
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>
+