/* -*- indent-tabs-mode: nil; js-indent-level: 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"); var Ci = Components.interfaces; var Cc = Components.classes; var Cu = Components.utils; var Cr = Components.results; Cu.import("resource://gre/modules/Services.jsm"); Cu.import('resource://gre/modules/XPCOMUtils.jsm'); XPCOMUtils.defineLazyServiceGetter(Services, "fm", "@mozilla.org/focus-manager;1", "nsIFocusManager"); /* * A WeakMap to map window to objects keeping it's TextInputProcessor instance. */ var WindowMap = { // WeakMap of pairs. _map: null, /* * Set the object associated to the window and return it. */ _getObjForWin: function(win) { if (!this._map) { this._map = new WeakMap(); } if (this._map.has(win)) { return this._map.get(win); } else { let obj = { tip: null }; this._map.set(win, obj); return obj; } }, getTextInputProcessor: function(win) { if (!win) { return; } let obj = this._getObjForWin(win); let tip = obj.tip if (!tip) { tip = obj.tip = Cc["@mozilla.org/text-input-processor;1"] .createInstance(Ci.nsITextInputProcessor); } if (!tip.beginInputTransaction(win, textInputProcessorCallback)) { tip = obj.tip = null; } return tip; } }; 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; var HTMLDocument = Ci.nsIDOMHTMLDocument; var HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; var HTMLBodyElement = Ci.nsIDOMHTMLBodyElement; var HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; var HTMLInputElement = Ci.nsIDOMHTMLInputElement; var HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; var HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; var HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement; var HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; function guessKeyNameFromKeyCode(KeyboardEvent, aKeyCode) { switch (aKeyCode) { case KeyboardEvent.DOM_VK_CANCEL: return "Cancel"; case KeyboardEvent.DOM_VK_HELP: return "Help"; case KeyboardEvent.DOM_VK_BACK_SPACE: return "Backspace"; case KeyboardEvent.DOM_VK_TAB: return "Tab"; case KeyboardEvent.DOM_VK_CLEAR: return "Clear"; case KeyboardEvent.DOM_VK_RETURN: return "Enter"; case KeyboardEvent.DOM_VK_SHIFT: return "Shift"; case KeyboardEvent.DOM_VK_CONTROL: return "Control"; case KeyboardEvent.DOM_VK_ALT: return "Alt"; case KeyboardEvent.DOM_VK_PAUSE: return "Pause"; case KeyboardEvent.DOM_VK_EISU: return "Eisu"; case KeyboardEvent.DOM_VK_ESCAPE: return "Escape"; case KeyboardEvent.DOM_VK_CONVERT: return "Convert"; case KeyboardEvent.DOM_VK_NONCONVERT: return "NonConvert"; case KeyboardEvent.DOM_VK_ACCEPT: return "Accept"; case KeyboardEvent.DOM_VK_MODECHANGE: return "ModeChange"; case KeyboardEvent.DOM_VK_PAGE_UP: return "PageUp"; case KeyboardEvent.DOM_VK_PAGE_DOWN: return "PageDown"; case KeyboardEvent.DOM_VK_END: return "End"; case KeyboardEvent.DOM_VK_HOME: return "Home"; case KeyboardEvent.DOM_VK_LEFT: return "ArrowLeft"; case KeyboardEvent.DOM_VK_UP: return "ArrowUp"; case KeyboardEvent.DOM_VK_RIGHT: return "ArrowRight"; case KeyboardEvent.DOM_VK_DOWN: return "ArrowDown"; case KeyboardEvent.DOM_VK_SELECT: return "Select"; case KeyboardEvent.DOM_VK_PRINT: return "Print"; case KeyboardEvent.DOM_VK_EXECUTE: return "Execute"; case KeyboardEvent.DOM_VK_PRINTSCREEN: return "PrintScreen"; case KeyboardEvent.DOM_VK_INSERT: return "Insert"; case KeyboardEvent.DOM_VK_DELETE: return "Delete"; case KeyboardEvent.DOM_VK_WIN: return "OS"; case KeyboardEvent.DOM_VK_CONTEXT_MENU: return "ContextMenu"; case KeyboardEvent.DOM_VK_SLEEP: return "Standby"; case KeyboardEvent.DOM_VK_F1: return "F1"; case KeyboardEvent.DOM_VK_F2: return "F2"; case KeyboardEvent.DOM_VK_F3: return "F3"; case KeyboardEvent.DOM_VK_F4: return "F4"; case KeyboardEvent.DOM_VK_F5: return "F5"; case KeyboardEvent.DOM_VK_F6: return "F6"; case KeyboardEvent.DOM_VK_F7: return "F7"; case KeyboardEvent.DOM_VK_F8: return "F8"; case KeyboardEvent.DOM_VK_F9: return "F9"; case KeyboardEvent.DOM_VK_F10: return "F10"; case KeyboardEvent.DOM_VK_F11: return "F11"; case KeyboardEvent.DOM_VK_F12: return "F12"; case KeyboardEvent.DOM_VK_F13: return "F13"; case KeyboardEvent.DOM_VK_F14: return "F14"; case KeyboardEvent.DOM_VK_F15: return "F15"; case KeyboardEvent.DOM_VK_F16: return "F16"; case KeyboardEvent.DOM_VK_F17: return "F17"; case KeyboardEvent.DOM_VK_F18: return "F18"; case KeyboardEvent.DOM_VK_F19: return "F19"; case KeyboardEvent.DOM_VK_F20: return "F20"; case KeyboardEvent.DOM_VK_F21: return "F21"; case KeyboardEvent.DOM_VK_F22: return "F22"; case KeyboardEvent.DOM_VK_F23: return "F23"; case KeyboardEvent.DOM_VK_F24: return "F24"; case KeyboardEvent.DOM_VK_NUM_LOCK: return "NumLock"; case KeyboardEvent.DOM_VK_SCROLL_LOCK: return "ScrollLock"; case KeyboardEvent.DOM_VK_VOLUME_MUTE: return "VolumeMute"; case KeyboardEvent.DOM_VK_VOLUME_DOWN: return "VolumeDown"; case KeyboardEvent.DOM_VK_VOLUME_UP: return "VolumeUp"; case KeyboardEvent.DOM_VK_META: return "Meta"; case KeyboardEvent.DOM_VK_ALTGR: return "AltGraph"; case KeyboardEvent.DOM_VK_ATTN: return "Attn"; case KeyboardEvent.DOM_VK_CRSEL: return "CrSel"; case KeyboardEvent.DOM_VK_EXSEL: return "ExSel"; case KeyboardEvent.DOM_VK_EREOF: return "EraseEof"; case KeyboardEvent.DOM_VK_PLAY: return "Play"; default: return "Unidentified"; } } var 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.scrollMinX || win.scrollMaxY != win.scrollMinY) { 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, offset.height ); 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; } }; // This object implements nsITextInputProcessorCallback var textInputProcessorCallback = { onNotify: function(aTextInputProcessor, aNotification) { try { switch (aNotification.type) { case "request-to-commit": // TODO: Send a notification through asyncMessage to the keyboard here. aTextInputProcessor.commitComposition(); break; case "request-to-cancel": // TODO: Send a notification through asyncMessage to the keyboard here. aTextInputProcessor.cancelComposition(); break; case "notify-detached": // TODO: Send a notification through asyncMessage to the keyboard here. break; // TODO: Manage _focusedElement for text input from here instead. // (except for 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.tagName.toLowerCase(); let inputType = (element.type || "").toLowerCase(); let value = element.value || ""; let max = element.max || ""; let min = element.min || ""; // Treat contenteditable element as a special text area field if (isContentEditable(element)) { type = "contenteditable"; inputType = "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 attributeInputType = element.getAttribute("type") || ""; if (attributeInputType) { let inputTypeLowerCase = attributeInputType.toLowerCase(); switch (inputTypeLowerCase) { case "datetime": case "datetime-local": case "month": case "week": case "range": inputType = inputTypeLowerCase; 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, "inputType": inputType, "inputMode": inputMode, "choices": getListForElement(element), "value": value, "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