Bug 1026997 - Use nsISelectionPrivate to track selection changes in forms.js. r=yxl

This commit is contained in:
Jan Jongboom 2014-07-08 03:31:00 -04:00
parent b1c674417f
commit 9d019f9823
4 changed files with 138 additions and 48 deletions

View File

@ -394,6 +394,11 @@ MozInputContext.prototype = {
return;
}
// Update context first before resolving promise to avoid race condition
if (json.selectioninfo) {
this.updateSelectionContext(json.selectioninfo);
}
switch (msg.name) {
case "Keyboard:SendKey:Result:OK":
resolver.resolve();

View File

@ -224,6 +224,7 @@ let FormAssistant = {
_documentEncoder: null,
_editor: null,
_editing: false,
_selectionPrivate: null,
get focusedElement() {
if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
@ -244,8 +245,6 @@ let FormAssistant = {
return;
if (this.focusedElement) {
this.focusedElement.removeEventListener('mousedown', this);
this.focusedElement.removeEventListener('mouseup', this);
this.focusedElement.removeEventListener('compositionend', this);
if (this._observer) {
this._observer.disconnect();
@ -254,6 +253,10 @@ let FormAssistant = {
if (!element) {
this.focusedElement.blur();
}
if (this._selectionPrivate) {
this._selectionPrivate.removeSelectionListener(this);
this._selectionPrivate = null;
}
}
this._documentEncoder = null;
@ -269,8 +272,6 @@ let FormAssistant = {
}
if (element) {
element.addEventListener('mousedown', this);
element.addEventListener('mouseup', this);
element.addEventListener('compositionend', this);
if (isContentEditable(element)) {
this._documentEncoder = getDocumentEncoder(element);
@ -280,6 +281,12 @@ let FormAssistant = {
// Add a nsIEditorObserver to monitor the text content of the focused
// element.
this._editor.addEditorObserver(this);
let selection = this._editor.selection;
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
@ -305,6 +312,10 @@ let FormAssistant = {
this.focusedElement = element;
},
notifySelectionChanged: function(aDocument, aSelection, aReason) {
this.updateSelection();
},
get documentEncoder() {
return this._documentEncoder;
},
@ -376,32 +387,6 @@ let FormAssistant = {
}
break;
case 'mousedown':
if (!this.focusedElement) {
break;
}
// We only listen for this event on the currently focused element.
// When the mouse goes down, note the cursor/selection position
this.updateSelection();
break;
case 'mouseup':
if (!this.focusedElement) {
break;
}
// We only listen for this event on the currently focused element.
// When the mouse goes up, see if the cursor has moved (or the
// selection changed) since the mouse went down. If it has, we
// need to tell the keyboard about it
range = getSelectionRange(this.focusedElement);
if (range[0] !== this.selectionStart ||
range[1] !== this.selectionEnd) {
this.updateSelection();
}
break;
case "resize":
if (!this.isKeyboardOpened)
return;
@ -423,25 +408,12 @@ let FormAssistant = {
}
break;
case "input":
if (this.focusedElement) {
// When the text content changes, notify the keyboard
this.updateSelection();
}
break;
case "keydown":
if (!this.focusedElement) {
break;
}
CompositionManager.endComposition('');
// We use 'setTimeout' to wait until the input element accomplishes the
// change in selection range.
content.setTimeout(function() {
this.updateSelection();
}.bind(this), 0);
break;
case "keyup":
@ -450,7 +422,6 @@ let FormAssistant = {
}
CompositionManager.endComposition('');
break;
case "compositionend":
@ -525,7 +496,8 @@ let FormAssistant = {
if (json.requestId && doKeypress) {
sendAsyncMessage("Forms:SendKey:Result:OK", {
requestId: json.requestId
requestId: json.requestId,
selectioninfo: this.getSelectionInfo()
});
}
else if (json.requestId && !doKeypress) {
@ -583,8 +555,6 @@ let FormAssistant = {
break;
}
this.updateSelection();
if (json.requestId) {
sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
requestId: json.requestId,
@ -651,6 +621,7 @@ let FormAssistant = {
json.clauses);
sendAsyncMessage("Forms:SetComposition:Result:OK", {
requestId: json.requestId,
selectioninfo: this.getSelectionInfo()
});
break;
}
@ -659,6 +630,7 @@ let FormAssistant = {
CompositionManager.endComposition(json.text);
sendAsyncMessage("Forms:EndComposition:Result:OK", {
requestId: json.requestId,
selectioninfo: this.getSelectionInfo()
});
break;
}
@ -757,6 +729,8 @@ let FormAssistant = {
};
},
_selectionTimeout: null,
// Notify when the selection range changes
updateSelection: function fa_updateSelection() {
if (!this.focusedElement) {
@ -764,7 +738,16 @@ let FormAssistant = {
}
let selectionInfo = this.getSelectionInfo();
if (selectionInfo.changed) {
sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
// A call to setSelectionRange on input field causes 2 selection changes
// one to [0,0] and one to actual value. Both are sent in same tick.
// Prevent firing two events in that scenario, always only use the last 1
if (this._selectionTimeout) {
content.clearTimeout(this._selectionTimeout);
}
this._selectionTimeout = content.setTimeout(function() {
sendAsyncMessage("Forms:SelectionChange", selectionInfo);
});
}
}
};

View File

@ -13,5 +13,6 @@ support-files =
[test_bug949059.html]
[test_bug960946.html]
[test_bug978918.html]
[test_bug1026997.html]
[test_delete_focused_element.html]
[test_sendkey_cancel.html]

View File

@ -0,0 +1,101 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1026997
-->
<head>
<title>SelectionChange on 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=1026997">Mozilla Bug 1026997</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 file_test_app.html.
function appFrameScript() {
let input = content.document.getElementById('test-input');
input.focus();
function next(start, end) {
input.setSelectionRange(start, end);
}
addMessageListener("test:KeyBoard:nextSelection", function(event) {
let json = event.json;
next(json[0], json[1]);
});
}
function runTest() {
let actions = [
[0, 4],
[1, 1],
[3, 3],
[2, 3]
];
let counter = 0;
let mm = null;
let ic = null;
let im = navigator.mozInputMethod;
im.oninputcontextchange = function() {
ok(true, 'inputcontextchange event was fired.');
im.oninputcontextchange = null;
ic = im.inputcontext;
if (!ic) {
ok(false, 'Should have a non-null inputcontext.');
inputmethod_cleanup();
return;
}
ic.onselectionchange = function() {
is(ic.selectionStart, actions[counter][0], "start");
is(ic.selectionEnd, actions[counter][1], "end");
if (++counter === actions.length) {
inputmethod_cleanup();
return;
}
next();
};
next();
};
// 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_app.html';
app.setAttribute('mozbrowser', true);
document.body.appendChild(app);
app.addEventListener('mozbrowserloadend', function() {
mm = SpecialPowers.getBrowserFrameMessageManager(app);
mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
next();
});
function next() {
if (ic && mm) {
mm.sendAsyncMessage('test:KeyBoard:nextSelection', actions[counter]);
}
}
}
</script>
</pre>
</body>
</html>