Bug 1059163 - Add a mutation observer to contenteditable elements to detect selection changes that nsISelectionPrivate misses. r=yxl
--- 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>
+