зеркало из https://github.com/mozilla/gecko-dev.git
Bug 844716 - Enable keyboard Apps to get/set selection range of the input field. r=timdream
This commit is contained in:
Родитель
81921f1171
Коммит
af7372e639
|
@ -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);
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче