зеркало из 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("resize", this, true, false);
|
||||||
addEventListener("submit", this, true, false);
|
addEventListener("submit", this, true, false);
|
||||||
addEventListener("pagehide", 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:Select:Choice", this);
|
||||||
addMessageListener("Forms:Input:Value", this);
|
addMessageListener("Forms:Input:Value", this);
|
||||||
addMessageListener("Forms:Select:Blur", this);
|
addMessageListener("Forms:Select:Blur", this);
|
||||||
|
addMessageListener("Forms:SetSelectionRange", this);
|
||||||
},
|
},
|
||||||
|
|
||||||
ignoredInputTypes: new Set([
|
ignoredInputTypes: new Set([
|
||||||
|
@ -195,8 +198,8 @@ let FormAssistant = {
|
||||||
]),
|
]),
|
||||||
|
|
||||||
isKeyboardOpened: false,
|
isKeyboardOpened: false,
|
||||||
selectionStart: 0,
|
selectionStart: -1,
|
||||||
selectionEnd: 0,
|
selectionEnd: -1,
|
||||||
scrollIntoViewTimeout: null,
|
scrollIntoViewTimeout: null,
|
||||||
_focusedElement: null,
|
_focusedElement: null,
|
||||||
_documentEncoder: null,
|
_documentEncoder: null,
|
||||||
|
@ -257,11 +260,14 @@ let FormAssistant = {
|
||||||
|
|
||||||
if (isContentEditable(target)) {
|
if (isContentEditable(target)) {
|
||||||
this.showKeyboard(this.getTopLevelEditable(target));
|
this.showKeyboard(this.getTopLevelEditable(target));
|
||||||
|
this.updateSelection();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFocusableElement(target))
|
if (this.isFocusableElement(target)) {
|
||||||
this.showKeyboard(target);
|
this.showKeyboard(target);
|
||||||
|
this.updateSelection();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pagehide":
|
case "pagehide":
|
||||||
|
@ -272,16 +278,17 @@ let FormAssistant = {
|
||||||
// fall through
|
// fall through
|
||||||
case "blur":
|
case "blur":
|
||||||
case "submit":
|
case "submit":
|
||||||
if (this.focusedElement)
|
if (this.focusedElement) {
|
||||||
this.hideKeyboard();
|
this.hideKeyboard();
|
||||||
|
this.selectionStart = -1;
|
||||||
|
this.selectionEnd = -1;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mousedown':
|
case 'mousedown':
|
||||||
// We only listen for this event on the currently focused element.
|
// We only listen for this event on the currently focused element.
|
||||||
// When the mouse goes down, note the cursor/selection position
|
// When the mouse goes down, note the cursor/selection position
|
||||||
range = getSelectionRange(this.focusedElement);
|
this.updateSelection();
|
||||||
this.selectionStart = range[0];
|
|
||||||
this.selectionEnd = range[1];
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mouseup':
|
case 'mouseup':
|
||||||
|
@ -293,6 +300,7 @@ let FormAssistant = {
|
||||||
if (range[0] !== this.selectionStart ||
|
if (range[0] !== this.selectionStart ||
|
||||||
range[1] !== this.selectionEnd) {
|
range[1] !== this.selectionEnd) {
|
||||||
this.sendKeyboardState(this.focusedElement);
|
this.sendKeyboardState(this.focusedElement);
|
||||||
|
this.updateSelection();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -316,6 +324,19 @@ let FormAssistant = {
|
||||||
}.bind(this), RESIZE_SCROLL_DELAY);
|
}.bind(this), RESIZE_SCROLL_DELAY);
|
||||||
}
|
}
|
||||||
break;
|
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);
|
this.setFocusedElement(null);
|
||||||
break;
|
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));
|
sendAsyncMessage("Forms:Input", getJSON(element));
|
||||||
return true;
|
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
|
// Get the selection range of contenteditable elements
|
||||||
let win = element.ownerDocument.defaultView;
|
let win = element.ownerDocument.defaultView;
|
||||||
let sel = win.getSelection();
|
let sel = win.getSelection();
|
||||||
|
start = getContentEditableSelectionStart(element, sel);
|
||||||
|
end = start + getContentEditableSelectionLength(element, sel);
|
||||||
|
}
|
||||||
|
return [start, end];
|
||||||
|
}
|
||||||
|
|
||||||
let range = win.document.createRange();
|
function getContentEditableSelectionStart(element, selection) {
|
||||||
range.setStart(element, 0);
|
let doc = element.ownerDocument;
|
||||||
range.setEnd(sel.anchorNode, sel.anchorOffset);
|
let range = doc.createRange();
|
||||||
let encoder = FormAssistant.documentEncoder;
|
range.setStart(element, 0);
|
||||||
|
range.setEnd(selection.anchorNode, selection.anchorOffset);
|
||||||
encoder.setRange(range);
|
let encoder = FormAssistant.documentEncoder;
|
||||||
start = encoder.encodeToString().length;
|
encoder.setRange(range);
|
||||||
|
return encoder.encodeToString().length;
|
||||||
encoder.setRange(sel.getRangeAt(0));
|
|
||||||
end = start + encoder.encodeToString().length;
|
|
||||||
}
|
|
||||||
return [start, end];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
let Keyboard = {
|
||||||
_messageManager: null,
|
_messageManager: null,
|
||||||
_messageNames: [
|
_messageNames: [
|
||||||
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions'
|
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
|
||||||
|
'SetSelectionRange'
|
||||||
],
|
],
|
||||||
|
|
||||||
get messageManager() {
|
get messageManager() {
|
||||||
|
@ -46,6 +47,7 @@ let Keyboard = {
|
||||||
let frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
|
let frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
|
||||||
let mm = frameLoader.messageManager;
|
let mm = frameLoader.messageManager;
|
||||||
mm.addMessageListener('Forms:Input', this);
|
mm.addMessageListener('Forms:Input', this);
|
||||||
|
mm.addMessageListener('Forms:SelectionChange', this);
|
||||||
|
|
||||||
// When not running apps OOP, we need to load forms.js here since this
|
// When not running apps OOP, we need to load forms.js here since this
|
||||||
// won't happen from dom/ipc/preload.js
|
// won't happen from dom/ipc/preload.js
|
||||||
|
@ -61,7 +63,7 @@ let Keyboard = {
|
||||||
receiveMessage: function keyboardReceiveMessage(msg) {
|
receiveMessage: function keyboardReceiveMessage(msg) {
|
||||||
// If we get a 'Keyboard:XXX' message, check that the sender has the
|
// If we get a 'Keyboard:XXX' message, check that the sender has the
|
||||||
// keyboard permission.
|
// keyboard permission.
|
||||||
if (msg.name != 'Forms:Input') {
|
if (msg.name.indexOf("Keyboard:") != -1) {
|
||||||
let mm;
|
let mm;
|
||||||
try {
|
try {
|
||||||
mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
|
mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
|
||||||
|
@ -87,6 +89,9 @@ let Keyboard = {
|
||||||
case 'Forms:Input':
|
case 'Forms:Input':
|
||||||
this.handleFormsInput(msg);
|
this.handleFormsInput(msg);
|
||||||
break;
|
break;
|
||||||
|
case 'Forms:SelectionChange':
|
||||||
|
this.handleFormsSelectionChange(msg);
|
||||||
|
break;
|
||||||
case 'Keyboard:SetValue':
|
case 'Keyboard:SetValue':
|
||||||
this.setValue(msg);
|
this.setValue(msg);
|
||||||
break;
|
break;
|
||||||
|
@ -99,6 +104,9 @@ let Keyboard = {
|
||||||
case 'Keyboard:SetSelectedOptions':
|
case 'Keyboard:SetSelectedOptions':
|
||||||
this.setSelectedOption(msg);
|
this.setSelectedOption(msg);
|
||||||
break;
|
break;
|
||||||
|
case 'Keyboard:SetSelectionRange':
|
||||||
|
this.setSelectionRange(msg);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -109,6 +117,13 @@ let Keyboard = {
|
||||||
ppmm.broadcastAsyncMessage('Keyboard:FocusChange', msg.data);
|
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) {
|
setSelectedOption: function keyboardSetSelectedOption(msg) {
|
||||||
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
|
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
|
||||||
},
|
},
|
||||||
|
@ -117,6 +132,10 @@ let Keyboard = {
|
||||||
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
|
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setSelectionRange: function keyboardSetSelectionRange(msg) {
|
||||||
|
this.messageManager.sendAsyncMessage('Forms:SetSelectionRange', msg.data);
|
||||||
|
},
|
||||||
|
|
||||||
setValue: function keyboardSetValue(msg) {
|
setValue: function keyboardSetValue(msg) {
|
||||||
this.messageManager.sendAsyncMessage('Forms:Input:Value', msg.data);
|
this.messageManager.sendAsyncMessage('Forms:Input:Value', msg.data);
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,21 +47,27 @@ MozKeyboard.prototype = {
|
||||||
|
|
||||||
Services.obs.addObserver(this, "inner-window-destroyed", false);
|
Services.obs.addObserver(this, "inner-window-destroyed", false);
|
||||||
cpmm.addMessageListener('Keyboard:FocusChange', this);
|
cpmm.addMessageListener('Keyboard:FocusChange', this);
|
||||||
|
cpmm.addMessageListener('Keyboard:SelectionChange', this);
|
||||||
|
|
||||||
this._window = win;
|
this._window = win;
|
||||||
this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
.getInterface(Ci.nsIDOMWindowUtils);
|
.getInterface(Ci.nsIDOMWindowUtils);
|
||||||
this.innerWindowID = this._utils.currentInnerWindowID;
|
this.innerWindowID = this._utils.currentInnerWindowID;
|
||||||
this._focusHandler = null;
|
this._focusHandler = null;
|
||||||
|
this._selectionHandler = null;
|
||||||
|
this._selectionStart = -1;
|
||||||
|
this._selectionEnd = -1;
|
||||||
},
|
},
|
||||||
|
|
||||||
uninit: function mozKeyboardUninit() {
|
uninit: function mozKeyboardUninit() {
|
||||||
Services.obs.removeObserver(this, "inner-window-destroyed");
|
Services.obs.removeObserver(this, "inner-window-destroyed");
|
||||||
cpmm.removeMessageListener('Keyboard:FocusChange', this);
|
cpmm.removeMessageListener('Keyboard:FocusChange', this);
|
||||||
|
cpmm.removeMessageListener('Keyboard:SelectionChange', this);
|
||||||
|
|
||||||
this._window = null;
|
this._window = null;
|
||||||
this._utils = null;
|
this._utils = null;
|
||||||
this._focusHandler = null;
|
this._focusHandler = null;
|
||||||
|
this._selectionHandler = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
sendKey: function mozKeyboardSendKey(keyCode, charCode) {
|
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() {
|
removeFocus: function mozKeyboardRemoveFocus() {
|
||||||
cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
|
cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
|
||||||
},
|
},
|
||||||
|
@ -102,17 +131,41 @@ MozKeyboard.prototype = {
|
||||||
},
|
},
|
||||||
|
|
||||||
receiveMessage: function mozKeyboardReceiveMessage(msg) {
|
receiveMessage: function mozKeyboardReceiveMessage(msg) {
|
||||||
let handler = this._focusHandler;
|
if (msg.name == "Keyboard:FocusChange") {
|
||||||
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
|
let msgJson = msg.json;
|
||||||
return;
|
if (msgJson.type != "blur") {
|
||||||
|
this._selectionStart = msgJson.selectionStart;
|
||||||
|
this._selectionEnd = msgJson.selectionEnd;
|
||||||
|
} else {
|
||||||
|
this._selectionStart = 0;
|
||||||
|
this._selectionEnd = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let detail = {
|
let handler = this._focusHandler;
|
||||||
"detail": msg.json
|
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
|
||||||
};
|
return;
|
||||||
|
|
||||||
let evt = new this._window.CustomEvent("focuschanged",
|
let detail = {
|
||||||
ObjectWrapper.wrap(detail, this._window));
|
"detail": msgJson
|
||||||
handler.handleEvent(evt);
|
};
|
||||||
|
|
||||||
|
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) {
|
observe: function mozKeyboardObserve(subject, topic, data) {
|
||||||
|
@ -123,4 +176,3 @@ MozKeyboard.prototype = {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozKeyboard]);
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozKeyboard]);
|
||||||
|
|
||||||
|
|
|
@ -40,4 +40,25 @@ interface nsIB2GKeyboard : nsISupports
|
||||||
void removeFocus();
|
void removeFocus();
|
||||||
|
|
||||||
attribute nsIDOMEventListener onfocuschange;
|
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);
|
||||||
};
|
};
|
||||||
|
|
Загрузка…
Ссылка в новой задаче