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