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