diff -r 000000000000 -r 6474c204b198 dom/inputmethod/forms.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dom/inputmethod/forms.js Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,1259 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+dump("###################################### forms.js loaded\n");
+
+let Ci = Components.interfaces;
+let Cc = Components.classes;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyServiceGetter(Services, "fm",
+ "@mozilla.org/focus-manager;1",
+ "nsIFocusManager");
+
+XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () {
+ return content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+});
+
+const RESIZE_SCROLL_DELAY = 20;
+// In content editable node, when there are hidden elements such as , it
+// may need more than one (usually less than 3 times) move/extend operations
+// to change the selection range. If we cannot change the selection range
+// with more than 20 opertations, we are likely being blocked and cannot change
+// the selection range any more.
+const MAX_BLOCKED_COUNT = 20;
+
+let HTMLDocument = Ci.nsIDOMHTMLDocument;
+let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
+let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
+let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
+let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
+let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
+let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
+let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
+let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
+
+let FormVisibility = {
+ /**
+ * Searches upwards in the DOM for an element that has been scrolled.
+ *
+ * @param {HTMLElement} node element to start search at.
+ * @return {Window|HTMLElement|Null} null when none are found window/element otherwise.
+ */
+ findScrolled: function fv_findScrolled(node) {
+ let win = node.ownerDocument.defaultView;
+
+ while (!(node instanceof HTMLBodyElement)) {
+
+ // We can skip elements that have not been scrolled.
+ // We only care about top now remember to add the scrollLeft
+ // check if we decide to care about the X axis.
+ if (node.scrollTop !== 0) {
+ // the element has been scrolled so we may need to adjust
+ // where we think the root element is located.
+ //
+ // Otherwise it may seem visible but be scrolled out of the viewport
+ // inside this scrollable node.
+ return node;
+ } else {
+ // this node does not effect where we think
+ // the node is even if it is scrollable it has not hidden
+ // the element we are looking for.
+ node = node.parentNode;
+ continue;
+ }
+ }
+
+ // we also care about the window this is the more
+ // common case where the content is larger then
+ // the viewport/screen.
+ if (win.scrollMaxX || win.scrollMaxY) {
+ return win;
+ }
+
+ return null;
+ },
+
+ /**
+ * Checks if "top and "bottom" points of the position is visible.
+ *
+ * @param {Number} top position.
+ * @param {Number} height of the element.
+ * @param {Number} maxHeight of the window.
+ * @return {Boolean} true when visible.
+ */
+ yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) {
+ return (top > 0 && (top + height) < maxHeight);
+ },
+
+ /**
+ * Searches up through the dom for scrollable elements
+ * which are not currently visible (relative to the viewport).
+ *
+ * @param {HTMLElement} element to start search at.
+ * @param {Object} pos .top, .height and .width of element.
+ */
+ scrollablesVisible: function fv_scrollablesVisible(element, pos) {
+ while ((element = this.findScrolled(element))) {
+ if (element.window && element.self === element)
+ break;
+
+ // remember getBoundingClientRect does not care
+ // about scrolling only where the element starts
+ // in the document.
+ let offset = element.getBoundingClientRect();
+
+ // the top of both the scrollable area and
+ // the form element itself are in the same document.
+ // We adjust the "top" so if the elements coordinates
+ // are relative to the viewport in the current document.
+ let adjustedTop = pos.top - offset.top;
+
+ let visible = this.yAxisVisible(
+ adjustedTop,
+ pos.height,
+ pos.width
+ );
+
+ if (!visible)
+ return false;
+
+ element = element.parentNode;
+ }
+
+ return true;
+ },
+
+ /**
+ * Verifies the element is visible in the viewport.
+ * Handles scrollable areas, frames and scrollable viewport(s) (windows).
+ *
+ * @param {HTMLElement} element to verify.
+ * @return {Boolean} true when visible.
+ */
+ isVisible: function fv_isVisible(element) {
+ // scrollable frames can be ignored we just care about iframes...
+ let rect = element.getBoundingClientRect();
+ let parent = element.ownerDocument.defaultView;
+
+ // used to calculate the inner position of frames / scrollables.
+ // The intent was to use this information to scroll either up or down.
+ // scrollIntoView(true) will _break_ some web content so we can't do
+ // this today. If we want that functionality we need to manually scroll
+ // the individual elements.
+ let pos = {
+ top: rect.top,
+ height: rect.height,
+ width: rect.width
+ };
+
+ let visible = true;
+
+ do {
+ let frame = parent.frameElement;
+ visible = visible &&
+ this.yAxisVisible(pos.top, pos.height, parent.innerHeight) &&
+ this.scrollablesVisible(element, pos);
+
+ // nothing we can do about this now...
+ // In the future we can use this information to scroll
+ // only the elements we need to at this point as we should
+ // have all the details we need to figure out how to scroll.
+ if (!visible)
+ return false;
+
+ if (frame) {
+ let frameRect = frame.getBoundingClientRect();
+
+ pos.top += frameRect.top + frame.clientTop;
+ }
+ } while (
+ (parent !== parent.parent) &&
+ (parent = parent.parent)
+ );
+
+ return visible;
+ }
+};
+
+let FormAssistant = {
+ init: function fa_init() {
+ addEventListener("focus", this, true, false);
+ addEventListener("blur", this, true, false);
+ addEventListener("resize", this, true, false);
+ addEventListener("submit", this, true, false);
+ addEventListener("pagehide", this, true, false);
+ addEventListener("beforeunload", this, true, false);
+ addEventListener("input", this, true, false);
+ addEventListener("keydown", this, true, false);
+ addEventListener("keyup", this, true, false);
+ addMessageListener("Forms:Select:Choice", this);
+ addMessageListener("Forms:Input:Value", this);
+ addMessageListener("Forms:Select:Blur", this);
+ addMessageListener("Forms:SetSelectionRange", this);
+ addMessageListener("Forms:ReplaceSurroundingText", this);
+ addMessageListener("Forms:GetText", this);
+ addMessageListener("Forms:Input:SendKey", this);
+ addMessageListener("Forms:GetContext", this);
+ addMessageListener("Forms:SetComposition", this);
+ addMessageListener("Forms:EndComposition", this);
+ },
+
+ ignoredInputTypes: new Set([
+ 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image',
+ 'range'
+ ]),
+
+ 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,
+ _documentEncoder: null,
+ _editor: null,
+ _editing: false,
+
+ get focusedElement() {
+ if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
+ this._focusedElement = null;
+
+ return this._focusedElement;
+ },
+
+ set focusedElement(val) {
+ this._focusCounter++;
+ this._focusedElement = val;
+ },
+
+ setFocusedElement: function fa_setFocusedElement(element) {
+ let self = this;
+
+ if (element === this.focusedElement)
+ 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();
+ this._observer = null;
+ }
+ if (!element) {
+ this.focusedElement.blur();
+ }
+ }
+
+ this._documentEncoder = null;
+ if (this._editor) {
+ // When the nsIFrame of the input element is reconstructed by
+ // CSS restyling, the editor observers are removed. Catch
+ // [nsIEditor.removeEditorObserver] failure exception if that
+ // happens.
+ try {
+ this._editor.removeEditorObserver(this);
+ } catch (e) {}
+ this._editor = null;
+ }
+
+ if (element) {
+ element.addEventListener('mousedown', this);
+ element.addEventListener('mouseup', this);
+ element.addEventListener('compositionend', this);
+ if (isContentEditable(element)) {
+ this._documentEncoder = getDocumentEncoder(element);
+ }
+ this._editor = getPlaintextEditor(element);
+ if (this._editor) {
+ // Add a nsIEditorObserver to monitor the text content of the focused
+ // element.
+ this._editor.addEditorObserver(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) {
+ var del = [].some.call(mutations, function(m) {
+ return [].some.call(m.removedNodes, function(n) {
+ return n.contains(element);
+ });
+ });
+ if (del && element === self.focusedElement) {
+ // item was deleted, fake a blur so all state gets set correctly
+ self.handleEvent({ target: element, type: "blur" });
+ }
+ });
+
+ this._observer.observe(element.ownerDocument.body, {
+ childList: true,
+ subtree: true
+ });
+ }
+
+ this.focusedElement = element;
+ },
+
+ get documentEncoder() {
+ return this._documentEncoder;
+ },
+
+ // Get the nsIPlaintextEditor object of current input field.
+ get editor() {
+ return this._editor;
+ },
+
+ // Implements nsIEditorObserver get notification when the text content of
+ // current input field has changed.
+ EditAction: function fa_editAction() {
+ if (this._editing) {
+ return;
+ }
+ this.sendKeyboardState(this.focusedElement);
+ },
+
+ handleEvent: function fa_handleEvent(evt) {
+ let target = evt.target;
+
+ let range = null;
+ switch (evt.type) {
+ case "focus":
+ if (!target) {
+ break;
+ }
+
+ // Focusing on Window, Document or iFrame should focus body
+ if (target instanceof HTMLHtmlElement) {
+ target = target.document.body;
+ } else if (target instanceof HTMLDocument) {
+ target = target.body;
+ } else if (target instanceof HTMLIFrameElement) {
+ target = target.contentDocument ? target.contentDocument.body
+ : null;
+ }
+
+ if (!target) {
+ break;
+ }
+
+ if (isContentEditable(target)) {
+ this.showKeyboard(this.getTopLevelEditable(target));
+ this.updateSelection();
+ break;
+ }
+
+ if (this.isFocusableElement(target)) {
+ this.showKeyboard(target);
+ this.updateSelection();
+ }
+ break;
+
+ case "pagehide":
+ case "beforeunload":
+ // We are only interested to the pagehide and beforeunload events from
+ // the root document.
+ if (target && target != content.document) {
+ break;
+ }
+ // fall through
+ case "blur":
+ case "submit":
+ if (this.focusedElement) {
+ this.hideKeyboard();
+ this.selectionStart = -1;
+ this.selectionEnd = -1;
+ }
+ 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;
+
+ if (this.scrollIntoViewTimeout) {
+ content.clearTimeout(this.scrollIntoViewTimeout);
+ this.scrollIntoViewTimeout = null;
+ }
+
+ // We may receive multiple resize events in quick succession, so wait
+ // a bit before scrolling the input element into view.
+ if (this.focusedElement) {
+ this.scrollIntoViewTimeout = content.setTimeout(function () {
+ this.scrollIntoViewTimeout = null;
+ if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
+ scrollSelectionOrElementIntoView(this.focusedElement);
+ }
+ }.bind(this), RESIZE_SCROLL_DELAY);
+ }
+ 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":
+ if (!this.focusedElement) {
+ break;
+ }
+
+ CompositionManager.endComposition('');
+
+ break;
+
+ case "compositionend":
+ if (!this.focusedElement) {
+ break;
+ }
+
+ CompositionManager.onCompositionEnd();
+ break;
+ }
+ },
+
+ receiveMessage: function fa_receiveMessage(msg) {
+ let target = this.focusedElement;
+ let json = msg.json;
+
+ // To not break mozKeyboard contextId is optional
+ if ('contextId' in json &&
+ json.contextId !== this._focusCounter &&
+ json.requestId) {
+ // Ignore messages that are meant for a previously focused element
+ sendAsyncMessage("Forms:SequenceError", {
+ requestId: json.requestId,
+ error: "Expected contextId " + this._focusCounter +
+ " but was " + json.contextId
+ });
+ return;
+ }
+
+ if (!target) {
+ switch (msg.name) {
+ case "Forms:GetText":
+ sendAsyncMessage("Forms:GetText:Result:Error", {
+ requestId: json.requestId,
+ error: "No focused element"
+ });
+ break;
+ }
+ return;
+ }
+
+ this._editing = true;
+ switch (msg.name) {
+ case "Forms:Input:Value": {
+ CompositionManager.endComposition('');
+
+ target.value = json.value;
+
+ let event = target.ownerDocument.createEvent('HTMLEvents');
+ event.initEvent('input', true, false);
+ target.dispatchEvent(event);
+ break;
+ }
+
+ case "Forms:Input:SendKey":
+ CompositionManager.endComposition('');
+
+ this._editing = true;
+ let doKeypress = domWindowUtils.sendKeyEvent('keydown', json.keyCode,
+ json.charCode, json.modifiers);
+ if (doKeypress) {
+ domWindowUtils.sendKeyEvent('keypress', json.keyCode,
+ json.charCode, json.modifiers);
+ }
+
+ if(!json.repeat) {
+ domWindowUtils.sendKeyEvent('keyup', json.keyCode,
+ json.charCode, json.modifiers);
+ }
+
+ this._editing = false;
+
+ if (json.requestId && doKeypress) {
+ sendAsyncMessage("Forms:SendKey:Result:OK", {
+ requestId: json.requestId
+ });
+ }
+ else if (json.requestId && !doKeypress) {
+ sendAsyncMessage("Forms:SendKey:Result:Error", {
+ requestId: json.requestId,
+ error: "Keydown event got canceled"
+ });
+ }
+ break;
+
+ case "Forms:Select:Choice":
+ let options = target.options;
+ let valueChanged = false;
+ if ("index" in json) {
+ if (options.selectedIndex != json.index) {
+ options.selectedIndex = json.index;
+ valueChanged = true;
+ }
+ } else if ("indexes" in json) {
+ for (let i = 0; i < options.length; i++) {
+ let newValue = (json.indexes.indexOf(i) != -1);
+ if (options.item(i).selected != newValue) {
+ options.item(i).selected = newValue;
+ valueChanged = true;
+ }
+ }
+ }
+
+ // only fire onchange event if any selected option is changed
+ if (valueChanged) {
+ let event = target.ownerDocument.createEvent('HTMLEvents');
+ event.initEvent('change', true, true);
+ target.dispatchEvent(event);
+ }
+ break;
+
+ case "Forms:Select:Blur": {
+ this.setFocusedElement(null);
+ break;
+ }
+
+ case "Forms:SetSelectionRange": {
+ CompositionManager.endComposition('');
+
+ let start = json.selectionStart;
+ let end = json.selectionEnd;
+
+ if (!setSelectionRange(target, start, end)) {
+ if (json.requestId) {
+ sendAsyncMessage("Forms:SetSelectionRange:Result:Error", {
+ requestId: json.requestId,
+ error: "failed"
+ });
+ }
+ break;
+ }
+
+ this.updateSelection();
+
+ if (json.requestId) {
+ sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ }
+ break;
+ }
+
+ case "Forms:ReplaceSurroundingText": {
+ CompositionManager.endComposition('');
+
+ let selectionRange = getSelectionRange(target);
+ if (!replaceSurroundingText(target,
+ json.text,
+ selectionRange[0],
+ selectionRange[1],
+ json.offset,
+ json.length)) {
+ if (json.requestId) {
+ sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", {
+ requestId: json.requestId,
+ error: "failed"
+ });
+ }
+ break;
+ }
+
+ if (json.requestId) {
+ sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ }
+ break;
+ }
+
+ case "Forms:GetText": {
+ let value = isContentEditable(target) ? getContentEditableText(target)
+ : target.value;
+
+ if (json.offset && json.length) {
+ value = value.substr(json.offset, json.length);
+ }
+ else if (json.offset) {
+ value = value.substr(json.offset);
+ }
+
+ sendAsyncMessage("Forms:GetText:Result:OK", {
+ requestId: json.requestId,
+ text: value
+ });
+ break;
+ }
+
+ case "Forms:GetContext": {
+ let obj = getJSON(target, this._focusCounter);
+ sendAsyncMessage("Forms:GetContext:Result:OK", obj);
+ break;
+ }
+
+ case "Forms:SetComposition": {
+ CompositionManager.setComposition(target, json.text, json.cursor,
+ json.clauses);
+ sendAsyncMessage("Forms:SetComposition:Result:OK", {
+ requestId: json.requestId,
+ });
+ break;
+ }
+
+ case "Forms:EndComposition": {
+ CompositionManager.endComposition(json.text);
+ sendAsyncMessage("Forms:EndComposition:Result:OK", {
+ requestId: json.requestId,
+ });
+ break;
+ }
+ }
+ this._editing = false;
+
+ },
+
+ showKeyboard: function fa_showKeyboard(target) {
+ if (this.focusedElement === target)
+ return;
+
+ if (target instanceof HTMLOptionElement)
+ target = target.parentNode;
+
+ this.setFocusedElement(target);
+
+ let kbOpened = this.sendKeyboardState(target);
+ if (this.isTextInputElement(target))
+ this.isKeyboardOpened = kbOpened;
+ },
+
+ hideKeyboard: function fa_hideKeyboard() {
+ sendAsyncMessage("Forms:Input", { "type": "blur" });
+ this.isKeyboardOpened = false;
+ this.setFocusedElement(null);
+ },
+
+ isFocusableElement: function fa_isFocusableElement(element) {
+ if (element instanceof HTMLSelectElement ||
+ element instanceof HTMLTextAreaElement)
+ return true;
+
+ if (element instanceof HTMLOptionElement &&
+ element.parentNode instanceof HTMLSelectElement)
+ return true;
+
+ return (element instanceof HTMLInputElement &&
+ !this.ignoredInputTypes.has(element.type));
+ },
+
+ isTextInputElement: function fa_isTextInputElement(element) {
+ return element instanceof HTMLInputElement ||
+ element instanceof HTMLTextAreaElement ||
+ isContentEditable(element);
+ },
+
+ getTopLevelEditable: function fa_getTopLevelEditable(element) {
+ function retrieveTopLevelEditable(element) {
+ while (element && !isContentEditable(element))
+ element = element.parentNode;
+
+ return element;
+ }
+
+ return retrieveTopLevelEditable(element) || element;
+ },
+
+ sendKeyboardState: function(element) {
+ // FIXME/bug 729623: work around apparent bug in the IME manager
+ // in gecko.
+ let readonly = element.getAttribute("readonly");
+ if (readonly) {
+ return false;
+ }
+
+ sendAsyncMessage("Forms:Input", getJSON(element, this._focusCounter));
+ return true;
+ },
+
+ getSelectionInfo: function fa_getSelectionInfo() {
+ let element = this.focusedElement;
+ let range = getSelectionRange(element);
+
+ let text = isContentEditable(element) ? getContentEditableText(element)
+ : element.value;
+
+ let textAround = getTextAroundCursor(text, range);
+
+ let changed = this.selectionStart !== range[0] ||
+ this.selectionEnd !== range[1] ||
+ this.textBeforeCursor !== textAround.before ||
+ this.textAfterCursor !== textAround.after;
+
+ this.selectionStart = range[0];
+ this.selectionEnd = range[1];
+ this.textBeforeCursor = textAround.before;
+ this.textAfterCursor = textAround.after;
+
+ return {
+ selectionStart: range[0],
+ selectionEnd: range[1],
+ textBeforeCursor: textAround.before,
+ textAfterCursor: textAround.after,
+ changed: changed
+ };
+ },
+
+ // Notify when the selection range changes
+ updateSelection: function fa_updateSelection() {
+ if (!this.focusedElement) {
+ return;
+ }
+ let selectionInfo = this.getSelectionInfo();
+ if (selectionInfo.changed) {
+ sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
+ }
+ }
+};
+
+FormAssistant.init();
+
+function isContentEditable(element) {
+ if (!element) {
+ return false;
+ }
+
+ if (element.isContentEditable || element.designMode == "on")
+ return true;
+
+ return element.ownerDocument && element.ownerDocument.designMode == "on";
+}
+
+function isPlainTextField(element) {
+ if (!element) {
+ return false;
+ }
+
+ return element instanceof HTMLTextAreaElement ||
+ (element instanceof HTMLInputElement &&
+ element.mozIsTextField(false));
+}
+
+function getJSON(element, focusCounter) {
+ // has a nested anonymous element that
+ // takes focus on behalf of the number control when someone tries to focus
+ // the number control. If |element| is such an anonymous text control then we
+ // need it's number control here in order to get the correct 'type' etc.:
+ element = element.ownerNumberControl || element;
+
+ let type = element.type || "";
+ let value = element.value || "";
+ let max = element.max || "";
+ let min = element.min || "";
+
+ // Treat contenteditble element as a special text area field
+ if (isContentEditable(element)) {
+ type = "textarea";
+ value = getContentEditableText(element);
+ }
+
+ // Until the input type=date/datetime/range have been implemented
+ // let's return their real type even if the platform returns 'text'
+ let attributeType = element.getAttribute("type") || "";
+
+ if (attributeType) {
+ var typeLowerCase = attributeType.toLowerCase();
+ switch (typeLowerCase) {
+ case "datetime":
+ case "datetime-local":
+ case "range":
+ type = typeLowerCase;
+ break;
+ }
+ }
+
+ // Gecko has some support for @inputmode but behind a preference and
+ // it is disabled by default.
+ // Gaia is then using @x-inputmode has its proprietary way to set
+ // inputmode for fields. This shouldn't be used outside of pre-installed
+ // apps because the attribute is going to disappear as soon as a definitive
+ // solution will be find.
+ let inputmode = element.getAttribute('x-inputmode');
+ if (inputmode) {
+ inputmode = inputmode.toLowerCase();
+ } else {
+ inputmode = '';
+ }
+
+ let range = getSelectionRange(element);
+ let textAround = getTextAroundCursor(value, range);
+
+ return {
+ "contextId": focusCounter,
+
+ "type": type.toLowerCase(),
+ "choices": getListForElement(element),
+ "value": value,
+ "inputmode": inputmode,
+ "selectionStart": range[0],
+ "selectionEnd": range[1],
+ "max": max,
+ "min": min,
+ "lang": element.lang || "",
+ "textBeforeCursor": textAround.before,
+ "textAfterCursor": textAround.after
+ };
+}
+
+function getTextAroundCursor(value, range) {
+ let textBeforeCursor = range[0] < 100 ?
+ value.substr(0, range[0]) :
+ value.substr(range[0] - 100, 100);
+
+ let textAfterCursor = range[1] + 100 > value.length ?
+ value.substr(range[0], value.length) :
+ value.substr(range[0], range[1] - range[0] + 100);
+
+ return {
+ before: textBeforeCursor,
+ after: textAfterCursor
+ };
+}
+
+function getListForElement(element) {
+ if (!(element instanceof HTMLSelectElement))
+ return null;
+
+ let optionIndex = 0;
+ let result = {
+ "multiple": element.multiple,
+ "choices": []
+ };
+
+ // Build up a flat JSON array of the choices.
+ // In HTML, it's possible for select element choices to be under a
+ // group header (but not recursively). We distinguish between headers
+ // and entries using the boolean "list.group".
+ let children = element.children;
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+
+ if (child instanceof HTMLOptGroupElement) {
+ result.choices.push({
+ "group": true,
+ "text": child.label || child.firstChild.data,
+ "disabled": child.disabled
+ });
+
+ let subchildren = child.children;
+ for (let j = 0; j < subchildren.length; j++) {
+ let subchild = subchildren[j];
+ result.choices.push({
+ "group": false,
+ "inGroup": true,
+ "text": subchild.text,
+ "disabled": child.disabled || subchild.disabled,
+ "selected": subchild.selected,
+ "optionIndex": optionIndex++
+ });
+ }
+ } else if (child instanceof HTMLOptionElement) {
+ result.choices.push({
+ "group": false,
+ "inGroup": false,
+ "text": child.text,
+ "disabled": child.disabled,
+ "selected": child.selected,
+ "optionIndex": optionIndex++
+ });
+ }
+ }
+
+ return result;
+};
+
+// Create a plain text document encode from the focused element.
+function getDocumentEncoder(element) {
+ let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
+ .createInstance(Ci.nsIDocumentEncoder);
+ let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent |
+ Ci.nsIDocumentEncoder.OutputRaw |
+ Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
+ // Bug 902847. Don't trim trailing spaces of a line.
+ Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces |
+ Ci.nsIDocumentEncoder.OutputLFLineBreak |
+ Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder;
+ encoder.init(element.ownerDocument, "text/plain", flags);
+ return encoder;
+}
+
+// Get the visible content text of a content editable element
+function getContentEditableText(element) {
+ if (!element || !isContentEditable(element)) {
+ return null;
+ }
+
+ let doc = element.ownerDocument;
+ let range = doc.createRange();
+ range.selectNodeContents(element);
+ let encoder = FormAssistant.documentEncoder;
+ encoder.setRange(range);
+ return encoder.encodeToString();
+}
+
+function getSelectionRange(element) {
+ let start = 0;
+ let end = 0;
+ if (isPlainTextField(element)) {
+ // Get the selection range of and