1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/inputmethod/forms.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1259 @@ 1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / 1.5 +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +dump("###################################### forms.js loaded\n"); 1.13 + 1.14 +let Ci = Components.interfaces; 1.15 +let Cc = Components.classes; 1.16 +let Cu = Components.utils; 1.17 + 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.20 +XPCOMUtils.defineLazyServiceGetter(Services, "fm", 1.21 + "@mozilla.org/focus-manager;1", 1.22 + "nsIFocusManager"); 1.23 + 1.24 +XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () { 1.25 + return content.QueryInterface(Ci.nsIInterfaceRequestor) 1.26 + .getInterface(Ci.nsIDOMWindowUtils); 1.27 +}); 1.28 + 1.29 +const RESIZE_SCROLL_DELAY = 20; 1.30 +// In content editable node, when there are hidden elements such as <br>, it 1.31 +// may need more than one (usually less than 3 times) move/extend operations 1.32 +// to change the selection range. If we cannot change the selection range 1.33 +// with more than 20 opertations, we are likely being blocked and cannot change 1.34 +// the selection range any more. 1.35 +const MAX_BLOCKED_COUNT = 20; 1.36 + 1.37 +let HTMLDocument = Ci.nsIDOMHTMLDocument; 1.38 +let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; 1.39 +let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement; 1.40 +let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; 1.41 +let HTMLInputElement = Ci.nsIDOMHTMLInputElement; 1.42 +let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; 1.43 +let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; 1.44 +let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement; 1.45 +let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; 1.46 + 1.47 +let FormVisibility = { 1.48 + /** 1.49 + * Searches upwards in the DOM for an element that has been scrolled. 1.50 + * 1.51 + * @param {HTMLElement} node element to start search at. 1.52 + * @return {Window|HTMLElement|Null} null when none are found window/element otherwise. 1.53 + */ 1.54 + findScrolled: function fv_findScrolled(node) { 1.55 + let win = node.ownerDocument.defaultView; 1.56 + 1.57 + while (!(node instanceof HTMLBodyElement)) { 1.58 + 1.59 + // We can skip elements that have not been scrolled. 1.60 + // We only care about top now remember to add the scrollLeft 1.61 + // check if we decide to care about the X axis. 1.62 + if (node.scrollTop !== 0) { 1.63 + // the element has been scrolled so we may need to adjust 1.64 + // where we think the root element is located. 1.65 + // 1.66 + // Otherwise it may seem visible but be scrolled out of the viewport 1.67 + // inside this scrollable node. 1.68 + return node; 1.69 + } else { 1.70 + // this node does not effect where we think 1.71 + // the node is even if it is scrollable it has not hidden 1.72 + // the element we are looking for. 1.73 + node = node.parentNode; 1.74 + continue; 1.75 + } 1.76 + } 1.77 + 1.78 + // we also care about the window this is the more 1.79 + // common case where the content is larger then 1.80 + // the viewport/screen. 1.81 + if (win.scrollMaxX || win.scrollMaxY) { 1.82 + return win; 1.83 + } 1.84 + 1.85 + return null; 1.86 + }, 1.87 + 1.88 + /** 1.89 + * Checks if "top and "bottom" points of the position is visible. 1.90 + * 1.91 + * @param {Number} top position. 1.92 + * @param {Number} height of the element. 1.93 + * @param {Number} maxHeight of the window. 1.94 + * @return {Boolean} true when visible. 1.95 + */ 1.96 + yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) { 1.97 + return (top > 0 && (top + height) < maxHeight); 1.98 + }, 1.99 + 1.100 + /** 1.101 + * Searches up through the dom for scrollable elements 1.102 + * which are not currently visible (relative to the viewport). 1.103 + * 1.104 + * @param {HTMLElement} element to start search at. 1.105 + * @param {Object} pos .top, .height and .width of element. 1.106 + */ 1.107 + scrollablesVisible: function fv_scrollablesVisible(element, pos) { 1.108 + while ((element = this.findScrolled(element))) { 1.109 + if (element.window && element.self === element) 1.110 + break; 1.111 + 1.112 + // remember getBoundingClientRect does not care 1.113 + // about scrolling only where the element starts 1.114 + // in the document. 1.115 + let offset = element.getBoundingClientRect(); 1.116 + 1.117 + // the top of both the scrollable area and 1.118 + // the form element itself are in the same document. 1.119 + // We adjust the "top" so if the elements coordinates 1.120 + // are relative to the viewport in the current document. 1.121 + let adjustedTop = pos.top - offset.top; 1.122 + 1.123 + let visible = this.yAxisVisible( 1.124 + adjustedTop, 1.125 + pos.height, 1.126 + pos.width 1.127 + ); 1.128 + 1.129 + if (!visible) 1.130 + return false; 1.131 + 1.132 + element = element.parentNode; 1.133 + } 1.134 + 1.135 + return true; 1.136 + }, 1.137 + 1.138 + /** 1.139 + * Verifies the element is visible in the viewport. 1.140 + * Handles scrollable areas, frames and scrollable viewport(s) (windows). 1.141 + * 1.142 + * @param {HTMLElement} element to verify. 1.143 + * @return {Boolean} true when visible. 1.144 + */ 1.145 + isVisible: function fv_isVisible(element) { 1.146 + // scrollable frames can be ignored we just care about iframes... 1.147 + let rect = element.getBoundingClientRect(); 1.148 + let parent = element.ownerDocument.defaultView; 1.149 + 1.150 + // used to calculate the inner position of frames / scrollables. 1.151 + // The intent was to use this information to scroll either up or down. 1.152 + // scrollIntoView(true) will _break_ some web content so we can't do 1.153 + // this today. If we want that functionality we need to manually scroll 1.154 + // the individual elements. 1.155 + let pos = { 1.156 + top: rect.top, 1.157 + height: rect.height, 1.158 + width: rect.width 1.159 + }; 1.160 + 1.161 + let visible = true; 1.162 + 1.163 + do { 1.164 + let frame = parent.frameElement; 1.165 + visible = visible && 1.166 + this.yAxisVisible(pos.top, pos.height, parent.innerHeight) && 1.167 + this.scrollablesVisible(element, pos); 1.168 + 1.169 + // nothing we can do about this now... 1.170 + // In the future we can use this information to scroll 1.171 + // only the elements we need to at this point as we should 1.172 + // have all the details we need to figure out how to scroll. 1.173 + if (!visible) 1.174 + return false; 1.175 + 1.176 + if (frame) { 1.177 + let frameRect = frame.getBoundingClientRect(); 1.178 + 1.179 + pos.top += frameRect.top + frame.clientTop; 1.180 + } 1.181 + } while ( 1.182 + (parent !== parent.parent) && 1.183 + (parent = parent.parent) 1.184 + ); 1.185 + 1.186 + return visible; 1.187 + } 1.188 +}; 1.189 + 1.190 +let FormAssistant = { 1.191 + init: function fa_init() { 1.192 + addEventListener("focus", this, true, false); 1.193 + addEventListener("blur", this, true, false); 1.194 + addEventListener("resize", this, true, false); 1.195 + addEventListener("submit", this, true, false); 1.196 + addEventListener("pagehide", this, true, false); 1.197 + addEventListener("beforeunload", this, true, false); 1.198 + addEventListener("input", this, true, false); 1.199 + addEventListener("keydown", this, true, false); 1.200 + addEventListener("keyup", this, true, false); 1.201 + addMessageListener("Forms:Select:Choice", this); 1.202 + addMessageListener("Forms:Input:Value", this); 1.203 + addMessageListener("Forms:Select:Blur", this); 1.204 + addMessageListener("Forms:SetSelectionRange", this); 1.205 + addMessageListener("Forms:ReplaceSurroundingText", this); 1.206 + addMessageListener("Forms:GetText", this); 1.207 + addMessageListener("Forms:Input:SendKey", this); 1.208 + addMessageListener("Forms:GetContext", this); 1.209 + addMessageListener("Forms:SetComposition", this); 1.210 + addMessageListener("Forms:EndComposition", this); 1.211 + }, 1.212 + 1.213 + ignoredInputTypes: new Set([ 1.214 + 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image', 1.215 + 'range' 1.216 + ]), 1.217 + 1.218 + isKeyboardOpened: false, 1.219 + selectionStart: -1, 1.220 + selectionEnd: -1, 1.221 + textBeforeCursor: "", 1.222 + textAfterCursor: "", 1.223 + scrollIntoViewTimeout: null, 1.224 + _focusedElement: null, 1.225 + _focusCounter: 0, // up one for every time we focus a new element 1.226 + _observer: null, 1.227 + _documentEncoder: null, 1.228 + _editor: null, 1.229 + _editing: false, 1.230 + 1.231 + get focusedElement() { 1.232 + if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement)) 1.233 + this._focusedElement = null; 1.234 + 1.235 + return this._focusedElement; 1.236 + }, 1.237 + 1.238 + set focusedElement(val) { 1.239 + this._focusCounter++; 1.240 + this._focusedElement = val; 1.241 + }, 1.242 + 1.243 + setFocusedElement: function fa_setFocusedElement(element) { 1.244 + let self = this; 1.245 + 1.246 + if (element === this.focusedElement) 1.247 + return; 1.248 + 1.249 + if (this.focusedElement) { 1.250 + this.focusedElement.removeEventListener('mousedown', this); 1.251 + this.focusedElement.removeEventListener('mouseup', this); 1.252 + this.focusedElement.removeEventListener('compositionend', this); 1.253 + if (this._observer) { 1.254 + this._observer.disconnect(); 1.255 + this._observer = null; 1.256 + } 1.257 + if (!element) { 1.258 + this.focusedElement.blur(); 1.259 + } 1.260 + } 1.261 + 1.262 + this._documentEncoder = null; 1.263 + if (this._editor) { 1.264 + // When the nsIFrame of the input element is reconstructed by 1.265 + // CSS restyling, the editor observers are removed. Catch 1.266 + // [nsIEditor.removeEditorObserver] failure exception if that 1.267 + // happens. 1.268 + try { 1.269 + this._editor.removeEditorObserver(this); 1.270 + } catch (e) {} 1.271 + this._editor = null; 1.272 + } 1.273 + 1.274 + if (element) { 1.275 + element.addEventListener('mousedown', this); 1.276 + element.addEventListener('mouseup', this); 1.277 + element.addEventListener('compositionend', this); 1.278 + if (isContentEditable(element)) { 1.279 + this._documentEncoder = getDocumentEncoder(element); 1.280 + } 1.281 + this._editor = getPlaintextEditor(element); 1.282 + if (this._editor) { 1.283 + // Add a nsIEditorObserver to monitor the text content of the focused 1.284 + // element. 1.285 + this._editor.addEditorObserver(this); 1.286 + } 1.287 + 1.288 + // If our focusedElement is removed from DOM we want to handle it properly 1.289 + let MutationObserver = element.ownerDocument.defaultView.MutationObserver; 1.290 + this._observer = new MutationObserver(function(mutations) { 1.291 + var del = [].some.call(mutations, function(m) { 1.292 + return [].some.call(m.removedNodes, function(n) { 1.293 + return n.contains(element); 1.294 + }); 1.295 + }); 1.296 + if (del && element === self.focusedElement) { 1.297 + // item was deleted, fake a blur so all state gets set correctly 1.298 + self.handleEvent({ target: element, type: "blur" }); 1.299 + } 1.300 + }); 1.301 + 1.302 + this._observer.observe(element.ownerDocument.body, { 1.303 + childList: true, 1.304 + subtree: true 1.305 + }); 1.306 + } 1.307 + 1.308 + this.focusedElement = element; 1.309 + }, 1.310 + 1.311 + get documentEncoder() { 1.312 + return this._documentEncoder; 1.313 + }, 1.314 + 1.315 + // Get the nsIPlaintextEditor object of current input field. 1.316 + get editor() { 1.317 + return this._editor; 1.318 + }, 1.319 + 1.320 + // Implements nsIEditorObserver get notification when the text content of 1.321 + // current input field has changed. 1.322 + EditAction: function fa_editAction() { 1.323 + if (this._editing) { 1.324 + return; 1.325 + } 1.326 + this.sendKeyboardState(this.focusedElement); 1.327 + }, 1.328 + 1.329 + handleEvent: function fa_handleEvent(evt) { 1.330 + let target = evt.target; 1.331 + 1.332 + let range = null; 1.333 + switch (evt.type) { 1.334 + case "focus": 1.335 + if (!target) { 1.336 + break; 1.337 + } 1.338 + 1.339 + // Focusing on Window, Document or iFrame should focus body 1.340 + if (target instanceof HTMLHtmlElement) { 1.341 + target = target.document.body; 1.342 + } else if (target instanceof HTMLDocument) { 1.343 + target = target.body; 1.344 + } else if (target instanceof HTMLIFrameElement) { 1.345 + target = target.contentDocument ? target.contentDocument.body 1.346 + : null; 1.347 + } 1.348 + 1.349 + if (!target) { 1.350 + break; 1.351 + } 1.352 + 1.353 + if (isContentEditable(target)) { 1.354 + this.showKeyboard(this.getTopLevelEditable(target)); 1.355 + this.updateSelection(); 1.356 + break; 1.357 + } 1.358 + 1.359 + if (this.isFocusableElement(target)) { 1.360 + this.showKeyboard(target); 1.361 + this.updateSelection(); 1.362 + } 1.363 + break; 1.364 + 1.365 + case "pagehide": 1.366 + case "beforeunload": 1.367 + // We are only interested to the pagehide and beforeunload events from 1.368 + // the root document. 1.369 + if (target && target != content.document) { 1.370 + break; 1.371 + } 1.372 + // fall through 1.373 + case "blur": 1.374 + case "submit": 1.375 + if (this.focusedElement) { 1.376 + this.hideKeyboard(); 1.377 + this.selectionStart = -1; 1.378 + this.selectionEnd = -1; 1.379 + } 1.380 + break; 1.381 + 1.382 + case 'mousedown': 1.383 + if (!this.focusedElement) { 1.384 + break; 1.385 + } 1.386 + 1.387 + // We only listen for this event on the currently focused element. 1.388 + // When the mouse goes down, note the cursor/selection position 1.389 + this.updateSelection(); 1.390 + break; 1.391 + 1.392 + case 'mouseup': 1.393 + if (!this.focusedElement) { 1.394 + break; 1.395 + } 1.396 + 1.397 + // We only listen for this event on the currently focused element. 1.398 + // When the mouse goes up, see if the cursor has moved (or the 1.399 + // selection changed) since the mouse went down. If it has, we 1.400 + // need to tell the keyboard about it 1.401 + range = getSelectionRange(this.focusedElement); 1.402 + if (range[0] !== this.selectionStart || 1.403 + range[1] !== this.selectionEnd) { 1.404 + this.updateSelection(); 1.405 + } 1.406 + break; 1.407 + 1.408 + case "resize": 1.409 + if (!this.isKeyboardOpened) 1.410 + return; 1.411 + 1.412 + if (this.scrollIntoViewTimeout) { 1.413 + content.clearTimeout(this.scrollIntoViewTimeout); 1.414 + this.scrollIntoViewTimeout = null; 1.415 + } 1.416 + 1.417 + // We may receive multiple resize events in quick succession, so wait 1.418 + // a bit before scrolling the input element into view. 1.419 + if (this.focusedElement) { 1.420 + this.scrollIntoViewTimeout = content.setTimeout(function () { 1.421 + this.scrollIntoViewTimeout = null; 1.422 + if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) { 1.423 + scrollSelectionOrElementIntoView(this.focusedElement); 1.424 + } 1.425 + }.bind(this), RESIZE_SCROLL_DELAY); 1.426 + } 1.427 + break; 1.428 + 1.429 + case "input": 1.430 + if (this.focusedElement) { 1.431 + // When the text content changes, notify the keyboard 1.432 + this.updateSelection(); 1.433 + } 1.434 + break; 1.435 + 1.436 + case "keydown": 1.437 + if (!this.focusedElement) { 1.438 + break; 1.439 + } 1.440 + 1.441 + CompositionManager.endComposition(''); 1.442 + 1.443 + // We use 'setTimeout' to wait until the input element accomplishes the 1.444 + // change in selection range. 1.445 + content.setTimeout(function() { 1.446 + this.updateSelection(); 1.447 + }.bind(this), 0); 1.448 + break; 1.449 + 1.450 + case "keyup": 1.451 + if (!this.focusedElement) { 1.452 + break; 1.453 + } 1.454 + 1.455 + CompositionManager.endComposition(''); 1.456 + 1.457 + break; 1.458 + 1.459 + case "compositionend": 1.460 + if (!this.focusedElement) { 1.461 + break; 1.462 + } 1.463 + 1.464 + CompositionManager.onCompositionEnd(); 1.465 + break; 1.466 + } 1.467 + }, 1.468 + 1.469 + receiveMessage: function fa_receiveMessage(msg) { 1.470 + let target = this.focusedElement; 1.471 + let json = msg.json; 1.472 + 1.473 + // To not break mozKeyboard contextId is optional 1.474 + if ('contextId' in json && 1.475 + json.contextId !== this._focusCounter && 1.476 + json.requestId) { 1.477 + // Ignore messages that are meant for a previously focused element 1.478 + sendAsyncMessage("Forms:SequenceError", { 1.479 + requestId: json.requestId, 1.480 + error: "Expected contextId " + this._focusCounter + 1.481 + " but was " + json.contextId 1.482 + }); 1.483 + return; 1.484 + } 1.485 + 1.486 + if (!target) { 1.487 + switch (msg.name) { 1.488 + case "Forms:GetText": 1.489 + sendAsyncMessage("Forms:GetText:Result:Error", { 1.490 + requestId: json.requestId, 1.491 + error: "No focused element" 1.492 + }); 1.493 + break; 1.494 + } 1.495 + return; 1.496 + } 1.497 + 1.498 + this._editing = true; 1.499 + switch (msg.name) { 1.500 + case "Forms:Input:Value": { 1.501 + CompositionManager.endComposition(''); 1.502 + 1.503 + target.value = json.value; 1.504 + 1.505 + let event = target.ownerDocument.createEvent('HTMLEvents'); 1.506 + event.initEvent('input', true, false); 1.507 + target.dispatchEvent(event); 1.508 + break; 1.509 + } 1.510 + 1.511 + case "Forms:Input:SendKey": 1.512 + CompositionManager.endComposition(''); 1.513 + 1.514 + this._editing = true; 1.515 + let doKeypress = domWindowUtils.sendKeyEvent('keydown', json.keyCode, 1.516 + json.charCode, json.modifiers); 1.517 + if (doKeypress) { 1.518 + domWindowUtils.sendKeyEvent('keypress', json.keyCode, 1.519 + json.charCode, json.modifiers); 1.520 + } 1.521 + 1.522 + if(!json.repeat) { 1.523 + domWindowUtils.sendKeyEvent('keyup', json.keyCode, 1.524 + json.charCode, json.modifiers); 1.525 + } 1.526 + 1.527 + this._editing = false; 1.528 + 1.529 + if (json.requestId && doKeypress) { 1.530 + sendAsyncMessage("Forms:SendKey:Result:OK", { 1.531 + requestId: json.requestId 1.532 + }); 1.533 + } 1.534 + else if (json.requestId && !doKeypress) { 1.535 + sendAsyncMessage("Forms:SendKey:Result:Error", { 1.536 + requestId: json.requestId, 1.537 + error: "Keydown event got canceled" 1.538 + }); 1.539 + } 1.540 + break; 1.541 + 1.542 + case "Forms:Select:Choice": 1.543 + let options = target.options; 1.544 + let valueChanged = false; 1.545 + if ("index" in json) { 1.546 + if (options.selectedIndex != json.index) { 1.547 + options.selectedIndex = json.index; 1.548 + valueChanged = true; 1.549 + } 1.550 + } else if ("indexes" in json) { 1.551 + for (let i = 0; i < options.length; i++) { 1.552 + let newValue = (json.indexes.indexOf(i) != -1); 1.553 + if (options.item(i).selected != newValue) { 1.554 + options.item(i).selected = newValue; 1.555 + valueChanged = true; 1.556 + } 1.557 + } 1.558 + } 1.559 + 1.560 + // only fire onchange event if any selected option is changed 1.561 + if (valueChanged) { 1.562 + let event = target.ownerDocument.createEvent('HTMLEvents'); 1.563 + event.initEvent('change', true, true); 1.564 + target.dispatchEvent(event); 1.565 + } 1.566 + break; 1.567 + 1.568 + case "Forms:Select:Blur": { 1.569 + this.setFocusedElement(null); 1.570 + break; 1.571 + } 1.572 + 1.573 + case "Forms:SetSelectionRange": { 1.574 + CompositionManager.endComposition(''); 1.575 + 1.576 + let start = json.selectionStart; 1.577 + let end = json.selectionEnd; 1.578 + 1.579 + if (!setSelectionRange(target, start, end)) { 1.580 + if (json.requestId) { 1.581 + sendAsyncMessage("Forms:SetSelectionRange:Result:Error", { 1.582 + requestId: json.requestId, 1.583 + error: "failed" 1.584 + }); 1.585 + } 1.586 + break; 1.587 + } 1.588 + 1.589 + this.updateSelection(); 1.590 + 1.591 + if (json.requestId) { 1.592 + sendAsyncMessage("Forms:SetSelectionRange:Result:OK", { 1.593 + requestId: json.requestId, 1.594 + selectioninfo: this.getSelectionInfo() 1.595 + }); 1.596 + } 1.597 + break; 1.598 + } 1.599 + 1.600 + case "Forms:ReplaceSurroundingText": { 1.601 + CompositionManager.endComposition(''); 1.602 + 1.603 + let selectionRange = getSelectionRange(target); 1.604 + if (!replaceSurroundingText(target, 1.605 + json.text, 1.606 + selectionRange[0], 1.607 + selectionRange[1], 1.608 + json.offset, 1.609 + json.length)) { 1.610 + if (json.requestId) { 1.611 + sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", { 1.612 + requestId: json.requestId, 1.613 + error: "failed" 1.614 + }); 1.615 + } 1.616 + break; 1.617 + } 1.618 + 1.619 + if (json.requestId) { 1.620 + sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", { 1.621 + requestId: json.requestId, 1.622 + selectioninfo: this.getSelectionInfo() 1.623 + }); 1.624 + } 1.625 + break; 1.626 + } 1.627 + 1.628 + case "Forms:GetText": { 1.629 + let value = isContentEditable(target) ? getContentEditableText(target) 1.630 + : target.value; 1.631 + 1.632 + if (json.offset && json.length) { 1.633 + value = value.substr(json.offset, json.length); 1.634 + } 1.635 + else if (json.offset) { 1.636 + value = value.substr(json.offset); 1.637 + } 1.638 + 1.639 + sendAsyncMessage("Forms:GetText:Result:OK", { 1.640 + requestId: json.requestId, 1.641 + text: value 1.642 + }); 1.643 + break; 1.644 + } 1.645 + 1.646 + case "Forms:GetContext": { 1.647 + let obj = getJSON(target, this._focusCounter); 1.648 + sendAsyncMessage("Forms:GetContext:Result:OK", obj); 1.649 + break; 1.650 + } 1.651 + 1.652 + case "Forms:SetComposition": { 1.653 + CompositionManager.setComposition(target, json.text, json.cursor, 1.654 + json.clauses); 1.655 + sendAsyncMessage("Forms:SetComposition:Result:OK", { 1.656 + requestId: json.requestId, 1.657 + }); 1.658 + break; 1.659 + } 1.660 + 1.661 + case "Forms:EndComposition": { 1.662 + CompositionManager.endComposition(json.text); 1.663 + sendAsyncMessage("Forms:EndComposition:Result:OK", { 1.664 + requestId: json.requestId, 1.665 + }); 1.666 + break; 1.667 + } 1.668 + } 1.669 + this._editing = false; 1.670 + 1.671 + }, 1.672 + 1.673 + showKeyboard: function fa_showKeyboard(target) { 1.674 + if (this.focusedElement === target) 1.675 + return; 1.676 + 1.677 + if (target instanceof HTMLOptionElement) 1.678 + target = target.parentNode; 1.679 + 1.680 + this.setFocusedElement(target); 1.681 + 1.682 + let kbOpened = this.sendKeyboardState(target); 1.683 + if (this.isTextInputElement(target)) 1.684 + this.isKeyboardOpened = kbOpened; 1.685 + }, 1.686 + 1.687 + hideKeyboard: function fa_hideKeyboard() { 1.688 + sendAsyncMessage("Forms:Input", { "type": "blur" }); 1.689 + this.isKeyboardOpened = false; 1.690 + this.setFocusedElement(null); 1.691 + }, 1.692 + 1.693 + isFocusableElement: function fa_isFocusableElement(element) { 1.694 + if (element instanceof HTMLSelectElement || 1.695 + element instanceof HTMLTextAreaElement) 1.696 + return true; 1.697 + 1.698 + if (element instanceof HTMLOptionElement && 1.699 + element.parentNode instanceof HTMLSelectElement) 1.700 + return true; 1.701 + 1.702 + return (element instanceof HTMLInputElement && 1.703 + !this.ignoredInputTypes.has(element.type)); 1.704 + }, 1.705 + 1.706 + isTextInputElement: function fa_isTextInputElement(element) { 1.707 + return element instanceof HTMLInputElement || 1.708 + element instanceof HTMLTextAreaElement || 1.709 + isContentEditable(element); 1.710 + }, 1.711 + 1.712 + getTopLevelEditable: function fa_getTopLevelEditable(element) { 1.713 + function retrieveTopLevelEditable(element) { 1.714 + while (element && !isContentEditable(element)) 1.715 + element = element.parentNode; 1.716 + 1.717 + return element; 1.718 + } 1.719 + 1.720 + return retrieveTopLevelEditable(element) || element; 1.721 + }, 1.722 + 1.723 + sendKeyboardState: function(element) { 1.724 + // FIXME/bug 729623: work around apparent bug in the IME manager 1.725 + // in gecko. 1.726 + let readonly = element.getAttribute("readonly"); 1.727 + if (readonly) { 1.728 + return false; 1.729 + } 1.730 + 1.731 + sendAsyncMessage("Forms:Input", getJSON(element, this._focusCounter)); 1.732 + return true; 1.733 + }, 1.734 + 1.735 + getSelectionInfo: function fa_getSelectionInfo() { 1.736 + let element = this.focusedElement; 1.737 + let range = getSelectionRange(element); 1.738 + 1.739 + let text = isContentEditable(element) ? getContentEditableText(element) 1.740 + : element.value; 1.741 + 1.742 + let textAround = getTextAroundCursor(text, range); 1.743 + 1.744 + let changed = this.selectionStart !== range[0] || 1.745 + this.selectionEnd !== range[1] || 1.746 + this.textBeforeCursor !== textAround.before || 1.747 + this.textAfterCursor !== textAround.after; 1.748 + 1.749 + this.selectionStart = range[0]; 1.750 + this.selectionEnd = range[1]; 1.751 + this.textBeforeCursor = textAround.before; 1.752 + this.textAfterCursor = textAround.after; 1.753 + 1.754 + return { 1.755 + selectionStart: range[0], 1.756 + selectionEnd: range[1], 1.757 + textBeforeCursor: textAround.before, 1.758 + textAfterCursor: textAround.after, 1.759 + changed: changed 1.760 + }; 1.761 + }, 1.762 + 1.763 + // Notify when the selection range changes 1.764 + updateSelection: function fa_updateSelection() { 1.765 + if (!this.focusedElement) { 1.766 + return; 1.767 + } 1.768 + let selectionInfo = this.getSelectionInfo(); 1.769 + if (selectionInfo.changed) { 1.770 + sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo()); 1.771 + } 1.772 + } 1.773 +}; 1.774 + 1.775 +FormAssistant.init(); 1.776 + 1.777 +function isContentEditable(element) { 1.778 + if (!element) { 1.779 + return false; 1.780 + } 1.781 + 1.782 + if (element.isContentEditable || element.designMode == "on") 1.783 + return true; 1.784 + 1.785 + return element.ownerDocument && element.ownerDocument.designMode == "on"; 1.786 +} 1.787 + 1.788 +function isPlainTextField(element) { 1.789 + if (!element) { 1.790 + return false; 1.791 + } 1.792 + 1.793 + return element instanceof HTMLTextAreaElement || 1.794 + (element instanceof HTMLInputElement && 1.795 + element.mozIsTextField(false)); 1.796 +} 1.797 + 1.798 +function getJSON(element, focusCounter) { 1.799 + // <input type=number> has a nested anonymous <input type=text> element that 1.800 + // takes focus on behalf of the number control when someone tries to focus 1.801 + // the number control. If |element| is such an anonymous text control then we 1.802 + // need it's number control here in order to get the correct 'type' etc.: 1.803 + element = element.ownerNumberControl || element; 1.804 + 1.805 + let type = element.type || ""; 1.806 + let value = element.value || ""; 1.807 + let max = element.max || ""; 1.808 + let min = element.min || ""; 1.809 + 1.810 + // Treat contenteditble element as a special text area field 1.811 + if (isContentEditable(element)) { 1.812 + type = "textarea"; 1.813 + value = getContentEditableText(element); 1.814 + } 1.815 + 1.816 + // Until the input type=date/datetime/range have been implemented 1.817 + // let's return their real type even if the platform returns 'text' 1.818 + let attributeType = element.getAttribute("type") || ""; 1.819 + 1.820 + if (attributeType) { 1.821 + var typeLowerCase = attributeType.toLowerCase(); 1.822 + switch (typeLowerCase) { 1.823 + case "datetime": 1.824 + case "datetime-local": 1.825 + case "range": 1.826 + type = typeLowerCase; 1.827 + break; 1.828 + } 1.829 + } 1.830 + 1.831 + // Gecko has some support for @inputmode but behind a preference and 1.832 + // it is disabled by default. 1.833 + // Gaia is then using @x-inputmode has its proprietary way to set 1.834 + // inputmode for fields. This shouldn't be used outside of pre-installed 1.835 + // apps because the attribute is going to disappear as soon as a definitive 1.836 + // solution will be find. 1.837 + let inputmode = element.getAttribute('x-inputmode'); 1.838 + if (inputmode) { 1.839 + inputmode = inputmode.toLowerCase(); 1.840 + } else { 1.841 + inputmode = ''; 1.842 + } 1.843 + 1.844 + let range = getSelectionRange(element); 1.845 + let textAround = getTextAroundCursor(value, range); 1.846 + 1.847 + return { 1.848 + "contextId": focusCounter, 1.849 + 1.850 + "type": type.toLowerCase(), 1.851 + "choices": getListForElement(element), 1.852 + "value": value, 1.853 + "inputmode": inputmode, 1.854 + "selectionStart": range[0], 1.855 + "selectionEnd": range[1], 1.856 + "max": max, 1.857 + "min": min, 1.858 + "lang": element.lang || "", 1.859 + "textBeforeCursor": textAround.before, 1.860 + "textAfterCursor": textAround.after 1.861 + }; 1.862 +} 1.863 + 1.864 +function getTextAroundCursor(value, range) { 1.865 + let textBeforeCursor = range[0] < 100 ? 1.866 + value.substr(0, range[0]) : 1.867 + value.substr(range[0] - 100, 100); 1.868 + 1.869 + let textAfterCursor = range[1] + 100 > value.length ? 1.870 + value.substr(range[0], value.length) : 1.871 + value.substr(range[0], range[1] - range[0] + 100); 1.872 + 1.873 + return { 1.874 + before: textBeforeCursor, 1.875 + after: textAfterCursor 1.876 + }; 1.877 +} 1.878 + 1.879 +function getListForElement(element) { 1.880 + if (!(element instanceof HTMLSelectElement)) 1.881 + return null; 1.882 + 1.883 + let optionIndex = 0; 1.884 + let result = { 1.885 + "multiple": element.multiple, 1.886 + "choices": [] 1.887 + }; 1.888 + 1.889 + // Build up a flat JSON array of the choices. 1.890 + // In HTML, it's possible for select element choices to be under a 1.891 + // group header (but not recursively). We distinguish between headers 1.892 + // and entries using the boolean "list.group". 1.893 + let children = element.children; 1.894 + for (let i = 0; i < children.length; i++) { 1.895 + let child = children[i]; 1.896 + 1.897 + if (child instanceof HTMLOptGroupElement) { 1.898 + result.choices.push({ 1.899 + "group": true, 1.900 + "text": child.label || child.firstChild.data, 1.901 + "disabled": child.disabled 1.902 + }); 1.903 + 1.904 + let subchildren = child.children; 1.905 + for (let j = 0; j < subchildren.length; j++) { 1.906 + let subchild = subchildren[j]; 1.907 + result.choices.push({ 1.908 + "group": false, 1.909 + "inGroup": true, 1.910 + "text": subchild.text, 1.911 + "disabled": child.disabled || subchild.disabled, 1.912 + "selected": subchild.selected, 1.913 + "optionIndex": optionIndex++ 1.914 + }); 1.915 + } 1.916 + } else if (child instanceof HTMLOptionElement) { 1.917 + result.choices.push({ 1.918 + "group": false, 1.919 + "inGroup": false, 1.920 + "text": child.text, 1.921 + "disabled": child.disabled, 1.922 + "selected": child.selected, 1.923 + "optionIndex": optionIndex++ 1.924 + }); 1.925 + } 1.926 + } 1.927 + 1.928 + return result; 1.929 +}; 1.930 + 1.931 +// Create a plain text document encode from the focused element. 1.932 +function getDocumentEncoder(element) { 1.933 + let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"] 1.934 + .createInstance(Ci.nsIDocumentEncoder); 1.935 + let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent | 1.936 + Ci.nsIDocumentEncoder.OutputRaw | 1.937 + Ci.nsIDocumentEncoder.OutputDropInvisibleBreak | 1.938 + // Bug 902847. Don't trim trailing spaces of a line. 1.939 + Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces | 1.940 + Ci.nsIDocumentEncoder.OutputLFLineBreak | 1.941 + Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder; 1.942 + encoder.init(element.ownerDocument, "text/plain", flags); 1.943 + return encoder; 1.944 +} 1.945 + 1.946 +// Get the visible content text of a content editable element 1.947 +function getContentEditableText(element) { 1.948 + if (!element || !isContentEditable(element)) { 1.949 + return null; 1.950 + } 1.951 + 1.952 + let doc = element.ownerDocument; 1.953 + let range = doc.createRange(); 1.954 + range.selectNodeContents(element); 1.955 + let encoder = FormAssistant.documentEncoder; 1.956 + encoder.setRange(range); 1.957 + return encoder.encodeToString(); 1.958 +} 1.959 + 1.960 +function getSelectionRange(element) { 1.961 + let start = 0; 1.962 + let end = 0; 1.963 + if (isPlainTextField(element)) { 1.964 + // Get the selection range of <input> and <textarea> elements 1.965 + start = element.selectionStart; 1.966 + end = element.selectionEnd; 1.967 + } else if (isContentEditable(element)){ 1.968 + // Get the selection range of contenteditable elements 1.969 + let win = element.ownerDocument.defaultView; 1.970 + let sel = win.getSelection(); 1.971 + if (sel && sel.rangeCount > 0) { 1.972 + start = getContentEditableSelectionStart(element, sel); 1.973 + end = start + getContentEditableSelectionLength(element, sel); 1.974 + } else { 1.975 + dump("Failed to get window.getSelection()\n"); 1.976 + } 1.977 + } 1.978 + return [start, end]; 1.979 + } 1.980 + 1.981 +function getContentEditableSelectionStart(element, selection) { 1.982 + let doc = element.ownerDocument; 1.983 + let range = doc.createRange(); 1.984 + range.setStart(element, 0); 1.985 + range.setEnd(selection.anchorNode, selection.anchorOffset); 1.986 + let encoder = FormAssistant.documentEncoder; 1.987 + encoder.setRange(range); 1.988 + return encoder.encodeToString().length; 1.989 +} 1.990 + 1.991 +function getContentEditableSelectionLength(element, selection) { 1.992 + let encoder = FormAssistant.documentEncoder; 1.993 + encoder.setRange(selection.getRangeAt(0)); 1.994 + return encoder.encodeToString().length; 1.995 +} 1.996 + 1.997 +function setSelectionRange(element, start, end) { 1.998 + let isTextField = isPlainTextField(element); 1.999 + 1.1000 + // Check the parameters 1.1001 + 1.1002 + if (!isTextField && !isContentEditable(element)) { 1.1003 + // Skip HTMLOptionElement and HTMLSelectElement elements, as they don't 1.1004 + // support the operation of setSelectionRange 1.1005 + return false; 1.1006 + } 1.1007 + 1.1008 + let text = isTextField ? element.value : getContentEditableText(element); 1.1009 + let length = text.length; 1.1010 + if (start < 0) { 1.1011 + start = 0; 1.1012 + } 1.1013 + if (end > length) { 1.1014 + end = length; 1.1015 + } 1.1016 + if (start > end) { 1.1017 + start = end; 1.1018 + } 1.1019 + 1.1020 + if (isTextField) { 1.1021 + // Set the selection range of <input> and <textarea> elements 1.1022 + element.setSelectionRange(start, end, "forward"); 1.1023 + return true; 1.1024 + } else { 1.1025 + // set the selection range of contenteditable elements 1.1026 + let win = element.ownerDocument.defaultView; 1.1027 + let sel = win.getSelection(); 1.1028 + 1.1029 + // Move the caret to the start position 1.1030 + sel.collapse(element, 0); 1.1031 + for (let i = 0; i < start; i++) { 1.1032 + sel.modify("move", "forward", "character"); 1.1033 + } 1.1034 + 1.1035 + // Avoid entering infinite loop in case we cannot change the selection 1.1036 + // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918 1.1037 + let oldStart = getContentEditableSelectionStart(element, sel); 1.1038 + let counter = 0; 1.1039 + while (oldStart < start) { 1.1040 + sel.modify("move", "forward", "character"); 1.1041 + let newStart = getContentEditableSelectionStart(element, sel); 1.1042 + if (oldStart == newStart) { 1.1043 + counter++; 1.1044 + if (counter > MAX_BLOCKED_COUNT) { 1.1045 + return false; 1.1046 + } 1.1047 + } else { 1.1048 + counter = 0; 1.1049 + oldStart = newStart; 1.1050 + } 1.1051 + } 1.1052 + 1.1053 + // Extend the selection to the end position 1.1054 + for (let i = start; i < end; i++) { 1.1055 + sel.modify("extend", "forward", "character"); 1.1056 + } 1.1057 + 1.1058 + // Avoid entering infinite loop in case we cannot change the selection 1.1059 + // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918 1.1060 + counter = 0; 1.1061 + let selectionLength = end - start; 1.1062 + let oldSelectionLength = getContentEditableSelectionLength(element, sel); 1.1063 + while (oldSelectionLength < selectionLength) { 1.1064 + sel.modify("extend", "forward", "character"); 1.1065 + let newSelectionLength = getContentEditableSelectionLength(element, sel); 1.1066 + if (oldSelectionLength == newSelectionLength ) { 1.1067 + counter++; 1.1068 + if (counter > MAX_BLOCKED_COUNT) { 1.1069 + return false; 1.1070 + } 1.1071 + } else { 1.1072 + counter = 0; 1.1073 + oldSelectionLength = newSelectionLength; 1.1074 + } 1.1075 + } 1.1076 + return true; 1.1077 + } 1.1078 +} 1.1079 + 1.1080 +/** 1.1081 + * Scroll the given element into view. 1.1082 + * 1.1083 + * Calls scrollSelectionIntoView for contentEditable elements. 1.1084 + */ 1.1085 +function scrollSelectionOrElementIntoView(element) { 1.1086 + let editor = getPlaintextEditor(element); 1.1087 + if (editor) { 1.1088 + editor.selectionController.scrollSelectionIntoView( 1.1089 + Ci.nsISelectionController.SELECTION_NORMAL, 1.1090 + Ci.nsISelectionController.SELECTION_FOCUS_REGION, 1.1091 + Ci.nsISelectionController.SCROLL_SYNCHRONOUS); 1.1092 + } else { 1.1093 + element.scrollIntoView(false); 1.1094 + } 1.1095 +} 1.1096 + 1.1097 +// Get nsIPlaintextEditor object from an input field 1.1098 +function getPlaintextEditor(element) { 1.1099 + let editor = null; 1.1100 + // Get nsIEditor 1.1101 + if (isPlainTextField(element)) { 1.1102 + // Get from the <input> and <textarea> elements 1.1103 + editor = element.QueryInterface(Ci.nsIDOMNSEditableElement).editor; 1.1104 + } else if (isContentEditable(element)) { 1.1105 + // Get from content editable element 1.1106 + let win = element.ownerDocument.defaultView; 1.1107 + let editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.1108 + .getInterface(Ci.nsIWebNavigation) 1.1109 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.1110 + .getInterface(Ci.nsIEditingSession); 1.1111 + if (editingSession) { 1.1112 + editor = editingSession.getEditorForWindow(win); 1.1113 + } 1.1114 + } 1.1115 + if (editor) { 1.1116 + editor.QueryInterface(Ci.nsIPlaintextEditor); 1.1117 + } 1.1118 + return editor; 1.1119 +} 1.1120 + 1.1121 +function replaceSurroundingText(element, text, selectionStart, selectionEnd, 1.1122 + offset, length) { 1.1123 + let editor = FormAssistant.editor; 1.1124 + if (!editor) { 1.1125 + return false; 1.1126 + } 1.1127 + 1.1128 + // Check the parameters. 1.1129 + let start = selectionStart + offset; 1.1130 + if (start < 0) { 1.1131 + start = 0; 1.1132 + } 1.1133 + if (length < 0) { 1.1134 + length = 0; 1.1135 + } 1.1136 + let end = start + length; 1.1137 + 1.1138 + if (selectionStart != start || selectionEnd != end) { 1.1139 + // Change selection range before replacing. 1.1140 + if (!setSelectionRange(element, start, end)) { 1.1141 + return false; 1.1142 + } 1.1143 + } 1.1144 + 1.1145 + if (start != end) { 1.1146 + // Delete the selected text. 1.1147 + editor.deleteSelection(Ci.nsIEditor.ePrevious, Ci.nsIEditor.eStrip); 1.1148 + } 1.1149 + 1.1150 + if (text) { 1.1151 + // We don't use CR but LF 1.1152 + // see https://bugzilla.mozilla.org/show_bug.cgi?id=902847 1.1153 + text = text.replace(/\r/g, '\n'); 1.1154 + // Insert the text to be replaced with. 1.1155 + editor.insertText(text); 1.1156 + } 1.1157 + return true; 1.1158 +} 1.1159 + 1.1160 +let CompositionManager = { 1.1161 + _isStarted: false, 1.1162 + _text: '', 1.1163 + _clauseAttrMap: { 1.1164 + 'raw-input': 1.1165 + Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT, 1.1166 + 'selected-raw-text': 1.1167 + Ci.nsICompositionStringSynthesizer.ATTR_SELECTEDRAWTEXT, 1.1168 + 'converted-text': 1.1169 + Ci.nsICompositionStringSynthesizer.ATTR_CONVERTEDTEXT, 1.1170 + 'selected-converted-text': 1.1171 + Ci.nsICompositionStringSynthesizer.ATTR_SELECTEDCONVERTEDTEXT 1.1172 + }, 1.1173 + 1.1174 + setComposition: function cm_setComposition(element, text, cursor, clauses) { 1.1175 + // Check parameters. 1.1176 + if (!element) { 1.1177 + return; 1.1178 + } 1.1179 + let len = text.length; 1.1180 + if (cursor > len) { 1.1181 + cursor = len; 1.1182 + } 1.1183 + let clauseLens = []; 1.1184 + let clauseAttrs = []; 1.1185 + if (clauses) { 1.1186 + let remainingLength = len; 1.1187 + for (let i = 0; i < clauses.length; i++) { 1.1188 + if (clauses[i]) { 1.1189 + let clauseLength = clauses[i].length || 0; 1.1190 + // Make sure the total clauses length is not bigger than that of the 1.1191 + // composition string. 1.1192 + if (clauseLength > remainingLength) { 1.1193 + clauseLength = remainingLength; 1.1194 + } 1.1195 + remainingLength -= clauseLength; 1.1196 + clauseLens.push(clauseLength); 1.1197 + clauseAttrs.push(this._clauseAttrMap[clauses[i].selectionType] || 1.1198 + Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT); 1.1199 + } 1.1200 + } 1.1201 + // If the total clauses length is less than that of the composition 1.1202 + // string, extend the last clause to the end of the composition string. 1.1203 + if (remainingLength > 0) { 1.1204 + clauseLens[clauseLens.length - 1] += remainingLength; 1.1205 + } 1.1206 + } else { 1.1207 + clauseLens.push(len); 1.1208 + clauseAttrs.push(Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT); 1.1209 + } 1.1210 + 1.1211 + // Start composition if need to. 1.1212 + if (!this._isStarted) { 1.1213 + this._isStarted = true; 1.1214 + domWindowUtils.sendCompositionEvent('compositionstart', '', ''); 1.1215 + this._text = ''; 1.1216 + } 1.1217 + 1.1218 + // Update the composing text. 1.1219 + if (this._text !== text) { 1.1220 + this._text = text; 1.1221 + domWindowUtils.sendCompositionEvent('compositionupdate', text, ''); 1.1222 + } 1.1223 + let compositionString = domWindowUtils.createCompositionStringSynthesizer(); 1.1224 + compositionString.setString(text); 1.1225 + for (var i = 0; i < clauseLens.length; i++) { 1.1226 + compositionString.appendClause(clauseLens[i], clauseAttrs[i]); 1.1227 + } 1.1228 + if (cursor >= 0) { 1.1229 + compositionString.setCaret(cursor, 0); 1.1230 + } 1.1231 + compositionString.dispatchEvent(); 1.1232 + }, 1.1233 + 1.1234 + endComposition: function cm_endComposition(text) { 1.1235 + if (!this._isStarted) { 1.1236 + return; 1.1237 + } 1.1238 + // Update the composing text. 1.1239 + if (this._text !== text) { 1.1240 + domWindowUtils.sendCompositionEvent('compositionupdate', text, ''); 1.1241 + } 1.1242 + let compositionString = domWindowUtils.createCompositionStringSynthesizer(); 1.1243 + compositionString.setString(text); 1.1244 + // Set the cursor position to |text.length| so that the text will be 1.1245 + // committed before the cursor position. 1.1246 + compositionString.setCaret(text.length, 0); 1.1247 + compositionString.dispatchEvent(); 1.1248 + domWindowUtils.sendCompositionEvent('compositionend', text, ''); 1.1249 + this._text = ''; 1.1250 + this._isStarted = false; 1.1251 + }, 1.1252 + 1.1253 + // Composition ends due to external actions. 1.1254 + onCompositionEnd: function cm_onCompositionEnd() { 1.1255 + if (!this._isStarted) { 1.1256 + return; 1.1257 + } 1.1258 + 1.1259 + this._text = ''; 1.1260 + this._isStarted = false; 1.1261 + } 1.1262 +};