Bug 844716 - Enable keyboard Apps to get/set selection range of the input field. r=timdream

This commit is contained in:
Yuan Xulei 2013-03-15 08:28:51 -04:00
Родитель 81921f1171
Коммит af7372e639
4 изменённых файлов: 227 добавлений и 31 удалений

Просмотреть файл

@ -185,9 +185,12 @@ let FormAssistant = {
addEventListener("resize", this, true, false);
addEventListener("submit", this, true, false);
addEventListener("pagehide", this, true, false);
addEventListener("input", this, true, false);
addEventListener("keydown", this, true, false);
addMessageListener("Forms:Select:Choice", this);
addMessageListener("Forms:Input:Value", this);
addMessageListener("Forms:Select:Blur", this);
addMessageListener("Forms:SetSelectionRange", this);
},
ignoredInputTypes: new Set([
@ -195,8 +198,8 @@ let FormAssistant = {
]),
isKeyboardOpened: false,
selectionStart: 0,
selectionEnd: 0,
selectionStart: -1,
selectionEnd: -1,
scrollIntoViewTimeout: null,
_focusedElement: null,
_documentEncoder: null,
@ -257,11 +260,14 @@ let FormAssistant = {
if (isContentEditable(target)) {
this.showKeyboard(this.getTopLevelEditable(target));
this.updateSelection();
break;
}
if (this.isFocusableElement(target))
if (this.isFocusableElement(target)) {
this.showKeyboard(target);
this.updateSelection();
}
break;
case "pagehide":
@ -272,16 +278,17 @@ let FormAssistant = {
// fall through
case "blur":
case "submit":
if (this.focusedElement)
if (this.focusedElement) {
this.hideKeyboard();
this.selectionStart = -1;
this.selectionEnd = -1;
}
break;
case 'mousedown':
// We only listen for this event on the currently focused element.
// When the mouse goes down, note the cursor/selection position
range = getSelectionRange(this.focusedElement);
this.selectionStart = range[0];
this.selectionEnd = range[1];
this.updateSelection();
break;
case 'mouseup':
@ -293,6 +300,7 @@ let FormAssistant = {
if (range[0] !== this.selectionStart ||
range[1] !== this.selectionEnd) {
this.sendKeyboardState(this.focusedElement);
this.updateSelection();
}
break;
@ -316,6 +324,19 @@ let FormAssistant = {
}.bind(this), RESIZE_SCROLL_DELAY);
}
break;
case "input":
// When the text content changes, notify the keyboard
this.updateSelection();
break;
case "keydown":
// We use 'setTimeout' to wait until the input element accomplishes the
// change in selection range
content.setTimeout(function() {
this.updateSelection();
}.bind(this), 0);
break;
}
},
@ -366,6 +387,14 @@ let FormAssistant = {
this.setFocusedElement(null);
break;
}
case "Forms:SetSelectionRange": {
let start = json.selectionStart;
let end = json.selectionEnd;
setSelectionRange(target, start, end);
this.updateSelection();
break;
}
}
},
@ -442,6 +471,19 @@ let FormAssistant = {
sendAsyncMessage("Forms:Input", getJSON(element));
return true;
},
// Notify when the selection range changes
updateSelection: function fa_updateSelection() {
let range = getSelectionRange(this.focusedElement);
if (range[0] != this.selectionStart || range[1] != this.selectionEnd) {
this.selectionStart = range[0];
this.selectionEnd = range[1];
sendAsyncMessage("Forms:SelectionChange", {
selectionStart: range[0],
selectionEnd: range[1]
});
}
}
};
@ -598,17 +640,79 @@ function getSelectionRange(element) {
// Get the selection range of contenteditable elements
let win = element.ownerDocument.defaultView;
let sel = win.getSelection();
start = getContentEditableSelectionStart(element, sel);
end = start + getContentEditableSelectionLength(element, sel);
}
return [start, end];
}
let range = win.document.createRange();
range.setStart(element, 0);
range.setEnd(sel.anchorNode, sel.anchorOffset);
let encoder = FormAssistant.documentEncoder;
encoder.setRange(range);
start = encoder.encodeToString().length;
encoder.setRange(sel.getRangeAt(0));
end = start + encoder.encodeToString().length;
}
return [start, end];
function getContentEditableSelectionStart(element, selection) {
let doc = element.ownerDocument;
let range = doc.createRange();
range.setStart(element, 0);
range.setEnd(selection.anchorNode, selection.anchorOffset);
let encoder = FormAssistant.documentEncoder;
encoder.setRange(range);
return encoder.encodeToString().length;
}
function getContentEditableSelectionLength(element, selection) {
let encoder = FormAssistant.documentEncoder;
encoder.setRange(selection.getRangeAt(0));
return encoder.encodeToString().length;
}
function setSelectionRange(element, start, end) {
let isPlainTextField = element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement;
// Check the parameters
if (!isPlainTextField && !isContentEditable(element)) {
// Skip HTMLOptionElement and HTMLSelectElement elements, as they don't
// support the operation of setSelectionRange
return;
}
let text = isPlainTextField ? element.value : getContentEditableText(element);
let length = text.length;
if (start < 0) {
start = 0;
}
if (end > length) {
end = length;
}
if (start > end) {
start = end;
}
if (isPlainTextField) {
// Set the selection range of <input> and <textarea> elements
element.setSelectionRange(start, end, "forward");
} 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) {
sel.modify("move", "forward", "character");
}
// Extend the selection to the end position
for (let i = start; i < end; i++) {
sel.modify("extend", "forward", "character");
}
let selectionLength = end - start;
while (getContentEditableSelectionLength(element, sel) < selectionLength) {
sel.modify("extend", "forward", "character");
}
}
}

Просмотреть файл

@ -20,7 +20,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
let Keyboard = {
_messageManager: null,
_messageNames: [
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions'
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
'SetSelectionRange'
],
get messageManager() {
@ -46,6 +47,7 @@ let Keyboard = {
let frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
let mm = frameLoader.messageManager;
mm.addMessageListener('Forms:Input', this);
mm.addMessageListener('Forms:SelectionChange', this);
// When not running apps OOP, we need to load forms.js here since this
// won't happen from dom/ipc/preload.js
@ -61,7 +63,7 @@ let Keyboard = {
receiveMessage: function keyboardReceiveMessage(msg) {
// If we get a 'Keyboard:XXX' message, check that the sender has the
// keyboard permission.
if (msg.name != 'Forms:Input') {
if (msg.name.indexOf("Keyboard:") != -1) {
let mm;
try {
mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
@ -87,6 +89,9 @@ let Keyboard = {
case 'Forms:Input':
this.handleFormsInput(msg);
break;
case 'Forms:SelectionChange':
this.handleFormsSelectionChange(msg);
break;
case 'Keyboard:SetValue':
this.setValue(msg);
break;
@ -99,6 +104,9 @@ let Keyboard = {
case 'Keyboard:SetSelectedOptions':
this.setSelectedOption(msg);
break;
case 'Keyboard:SetSelectionRange':
this.setSelectionRange(msg);
break;
}
},
@ -109,6 +117,13 @@ let Keyboard = {
ppmm.broadcastAsyncMessage('Keyboard:FocusChange', msg.data);
},
handleFormsSelectionChange: function keyboardHandleFormsSelectionChange(msg) {
this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
.frameLoader.messageManager;
ppmm.broadcastAsyncMessage('Keyboard:SelectionChange', msg.data);
},
setSelectedOption: function keyboardSetSelectedOption(msg) {
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
},
@ -117,6 +132,10 @@ let Keyboard = {
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
},
setSelectionRange: function keyboardSetSelectionRange(msg) {
this.messageManager.sendAsyncMessage('Forms:SetSelectionRange', msg.data);
},
setValue: function keyboardSetValue(msg) {
this.messageManager.sendAsyncMessage('Forms:Input:Value', msg.data);
},

Просмотреть файл

@ -47,21 +47,27 @@ MozKeyboard.prototype = {
Services.obs.addObserver(this, "inner-window-destroyed", false);
cpmm.addMessageListener('Keyboard:FocusChange', this);
cpmm.addMessageListener('Keyboard:SelectionChange', this);
this._window = win;
this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
this.innerWindowID = this._utils.currentInnerWindowID;
this._focusHandler = null;
this._selectionHandler = null;
this._selectionStart = -1;
this._selectionEnd = -1;
},
uninit: function mozKeyboardUninit() {
Services.obs.removeObserver(this, "inner-window-destroyed");
cpmm.removeMessageListener('Keyboard:FocusChange', this);
cpmm.removeMessageListener('Keyboard:SelectionChange', this);
this._window = null;
this._utils = null;
this._focusHandler = null;
this._selectionHandler = null;
},
sendKey: function mozKeyboardSendKey(keyCode, charCode) {
@ -89,6 +95,29 @@ MozKeyboard.prototype = {
});
},
set onselectionchange(val) {
this._selectionHandler = val;
},
get onselectionchange() {
return this._selectionHandler;
},
get selectionStart() {
return this._selectionStart;
},
get selectionEnd() {
return this._selectionEnd;
},
setSelectionRange: function mozKeyboardSetSelectionRange(start, end) {
cpmm.sendAsyncMessage('Keyboard:SetSelectionRange', {
'selectionStart': start,
'selectionEnd': end
});
},
removeFocus: function mozKeyboardRemoveFocus() {
cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
},
@ -102,17 +131,41 @@ MozKeyboard.prototype = {
},
receiveMessage: function mozKeyboardReceiveMessage(msg) {
let handler = this._focusHandler;
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
if (msg.name == "Keyboard:FocusChange") {
let msgJson = msg.json;
if (msgJson.type != "blur") {
this._selectionStart = msgJson.selectionStart;
this._selectionEnd = msgJson.selectionEnd;
} else {
this._selectionStart = 0;
this._selectionEnd = 0;
}
let detail = {
"detail": msg.json
};
let handler = this._focusHandler;
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
let evt = new this._window.CustomEvent("focuschanged",
ObjectWrapper.wrap(detail, this._window));
handler.handleEvent(evt);
let detail = {
"detail": msgJson
};
let evt = new this._window.CustomEvent("focuschanged",
ObjectWrapper.wrap(detail, this._window));
handler.handleEvent(evt);
} else if (msg.name == "Keyboard:SelectionChange") {
let msgJson = msg.json;
this._selectionStart = msgJson.selectionStart;
this._selectionEnd = msgJson.selectionEnd;
let handler = this._selectionHandler;
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
let evt = new this._window.CustomEvent("selectionchange",
ObjectWrapper.wrap({}, this._window));
handler.handleEvent(evt);
}
},
observe: function mozKeyboardObserve(subject, topic, data) {
@ -123,4 +176,3 @@ MozKeyboard.prototype = {
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozKeyboard]);

Просмотреть файл

@ -40,4 +40,25 @@ interface nsIB2GKeyboard : nsISupports
void removeFocus();
attribute nsIDOMEventListener onfocuschange;
// Fires when user moves the cursor, changes the selection, or alters the
// composing text length
attribute nsIDOMEventListener onselectionchange;
// The start position of the selection.
readonly attribute long selectionStart;
// The stop position of the selection.
readonly attribute long selectionEnd;
/*
* Set the selection range of the the editable text.
*
* @param start The beginning of the selected text.
* @param end The end of the selected text.
*
* Note that the start position should be less or equal to the end position.
* To move the cursor, set the start and end position to the same value.
*/
void setSelectionRange(in long start, in long end);
};