dom/inputmethod/forms.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
michael@0 2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 dump("###################################### forms.js loaded\n");
michael@0 10
michael@0 11 let Ci = Components.interfaces;
michael@0 12 let Cc = Components.classes;
michael@0 13 let Cu = Components.utils;
michael@0 14
michael@0 15 Cu.import("resource://gre/modules/Services.jsm");
michael@0 16 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
michael@0 17 XPCOMUtils.defineLazyServiceGetter(Services, "fm",
michael@0 18 "@mozilla.org/focus-manager;1",
michael@0 19 "nsIFocusManager");
michael@0 20
michael@0 21 XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () {
michael@0 22 return content.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 23 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 24 });
michael@0 25
michael@0 26 const RESIZE_SCROLL_DELAY = 20;
michael@0 27 // In content editable node, when there are hidden elements such as <br>, it
michael@0 28 // may need more than one (usually less than 3 times) move/extend operations
michael@0 29 // to change the selection range. If we cannot change the selection range
michael@0 30 // with more than 20 opertations, we are likely being blocked and cannot change
michael@0 31 // the selection range any more.
michael@0 32 const MAX_BLOCKED_COUNT = 20;
michael@0 33
michael@0 34 let HTMLDocument = Ci.nsIDOMHTMLDocument;
michael@0 35 let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
michael@0 36 let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
michael@0 37 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
michael@0 38 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
michael@0 39 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
michael@0 40 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
michael@0 41 let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
michael@0 42 let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
michael@0 43
michael@0 44 let FormVisibility = {
michael@0 45 /**
michael@0 46 * Searches upwards in the DOM for an element that has been scrolled.
michael@0 47 *
michael@0 48 * @param {HTMLElement} node element to start search at.
michael@0 49 * @return {Window|HTMLElement|Null} null when none are found window/element otherwise.
michael@0 50 */
michael@0 51 findScrolled: function fv_findScrolled(node) {
michael@0 52 let win = node.ownerDocument.defaultView;
michael@0 53
michael@0 54 while (!(node instanceof HTMLBodyElement)) {
michael@0 55
michael@0 56 // We can skip elements that have not been scrolled.
michael@0 57 // We only care about top now remember to add the scrollLeft
michael@0 58 // check if we decide to care about the X axis.
michael@0 59 if (node.scrollTop !== 0) {
michael@0 60 // the element has been scrolled so we may need to adjust
michael@0 61 // where we think the root element is located.
michael@0 62 //
michael@0 63 // Otherwise it may seem visible but be scrolled out of the viewport
michael@0 64 // inside this scrollable node.
michael@0 65 return node;
michael@0 66 } else {
michael@0 67 // this node does not effect where we think
michael@0 68 // the node is even if it is scrollable it has not hidden
michael@0 69 // the element we are looking for.
michael@0 70 node = node.parentNode;
michael@0 71 continue;
michael@0 72 }
michael@0 73 }
michael@0 74
michael@0 75 // we also care about the window this is the more
michael@0 76 // common case where the content is larger then
michael@0 77 // the viewport/screen.
michael@0 78 if (win.scrollMaxX || win.scrollMaxY) {
michael@0 79 return win;
michael@0 80 }
michael@0 81
michael@0 82 return null;
michael@0 83 },
michael@0 84
michael@0 85 /**
michael@0 86 * Checks if "top and "bottom" points of the position is visible.
michael@0 87 *
michael@0 88 * @param {Number} top position.
michael@0 89 * @param {Number} height of the element.
michael@0 90 * @param {Number} maxHeight of the window.
michael@0 91 * @return {Boolean} true when visible.
michael@0 92 */
michael@0 93 yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) {
michael@0 94 return (top > 0 && (top + height) < maxHeight);
michael@0 95 },
michael@0 96
michael@0 97 /**
michael@0 98 * Searches up through the dom for scrollable elements
michael@0 99 * which are not currently visible (relative to the viewport).
michael@0 100 *
michael@0 101 * @param {HTMLElement} element to start search at.
michael@0 102 * @param {Object} pos .top, .height and .width of element.
michael@0 103 */
michael@0 104 scrollablesVisible: function fv_scrollablesVisible(element, pos) {
michael@0 105 while ((element = this.findScrolled(element))) {
michael@0 106 if (element.window && element.self === element)
michael@0 107 break;
michael@0 108
michael@0 109 // remember getBoundingClientRect does not care
michael@0 110 // about scrolling only where the element starts
michael@0 111 // in the document.
michael@0 112 let offset = element.getBoundingClientRect();
michael@0 113
michael@0 114 // the top of both the scrollable area and
michael@0 115 // the form element itself are in the same document.
michael@0 116 // We adjust the "top" so if the elements coordinates
michael@0 117 // are relative to the viewport in the current document.
michael@0 118 let adjustedTop = pos.top - offset.top;
michael@0 119
michael@0 120 let visible = this.yAxisVisible(
michael@0 121 adjustedTop,
michael@0 122 pos.height,
michael@0 123 pos.width
michael@0 124 );
michael@0 125
michael@0 126 if (!visible)
michael@0 127 return false;
michael@0 128
michael@0 129 element = element.parentNode;
michael@0 130 }
michael@0 131
michael@0 132 return true;
michael@0 133 },
michael@0 134
michael@0 135 /**
michael@0 136 * Verifies the element is visible in the viewport.
michael@0 137 * Handles scrollable areas, frames and scrollable viewport(s) (windows).
michael@0 138 *
michael@0 139 * @param {HTMLElement} element to verify.
michael@0 140 * @return {Boolean} true when visible.
michael@0 141 */
michael@0 142 isVisible: function fv_isVisible(element) {
michael@0 143 // scrollable frames can be ignored we just care about iframes...
michael@0 144 let rect = element.getBoundingClientRect();
michael@0 145 let parent = element.ownerDocument.defaultView;
michael@0 146
michael@0 147 // used to calculate the inner position of frames / scrollables.
michael@0 148 // The intent was to use this information to scroll either up or down.
michael@0 149 // scrollIntoView(true) will _break_ some web content so we can't do
michael@0 150 // this today. If we want that functionality we need to manually scroll
michael@0 151 // the individual elements.
michael@0 152 let pos = {
michael@0 153 top: rect.top,
michael@0 154 height: rect.height,
michael@0 155 width: rect.width
michael@0 156 };
michael@0 157
michael@0 158 let visible = true;
michael@0 159
michael@0 160 do {
michael@0 161 let frame = parent.frameElement;
michael@0 162 visible = visible &&
michael@0 163 this.yAxisVisible(pos.top, pos.height, parent.innerHeight) &&
michael@0 164 this.scrollablesVisible(element, pos);
michael@0 165
michael@0 166 // nothing we can do about this now...
michael@0 167 // In the future we can use this information to scroll
michael@0 168 // only the elements we need to at this point as we should
michael@0 169 // have all the details we need to figure out how to scroll.
michael@0 170 if (!visible)
michael@0 171 return false;
michael@0 172
michael@0 173 if (frame) {
michael@0 174 let frameRect = frame.getBoundingClientRect();
michael@0 175
michael@0 176 pos.top += frameRect.top + frame.clientTop;
michael@0 177 }
michael@0 178 } while (
michael@0 179 (parent !== parent.parent) &&
michael@0 180 (parent = parent.parent)
michael@0 181 );
michael@0 182
michael@0 183 return visible;
michael@0 184 }
michael@0 185 };
michael@0 186
michael@0 187 let FormAssistant = {
michael@0 188 init: function fa_init() {
michael@0 189 addEventListener("focus", this, true, false);
michael@0 190 addEventListener("blur", this, true, false);
michael@0 191 addEventListener("resize", this, true, false);
michael@0 192 addEventListener("submit", this, true, false);
michael@0 193 addEventListener("pagehide", this, true, false);
michael@0 194 addEventListener("beforeunload", this, true, false);
michael@0 195 addEventListener("input", this, true, false);
michael@0 196 addEventListener("keydown", this, true, false);
michael@0 197 addEventListener("keyup", this, true, false);
michael@0 198 addMessageListener("Forms:Select:Choice", this);
michael@0 199 addMessageListener("Forms:Input:Value", this);
michael@0 200 addMessageListener("Forms:Select:Blur", this);
michael@0 201 addMessageListener("Forms:SetSelectionRange", this);
michael@0 202 addMessageListener("Forms:ReplaceSurroundingText", this);
michael@0 203 addMessageListener("Forms:GetText", this);
michael@0 204 addMessageListener("Forms:Input:SendKey", this);
michael@0 205 addMessageListener("Forms:GetContext", this);
michael@0 206 addMessageListener("Forms:SetComposition", this);
michael@0 207 addMessageListener("Forms:EndComposition", this);
michael@0 208 },
michael@0 209
michael@0 210 ignoredInputTypes: new Set([
michael@0 211 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image',
michael@0 212 'range'
michael@0 213 ]),
michael@0 214
michael@0 215 isKeyboardOpened: false,
michael@0 216 selectionStart: -1,
michael@0 217 selectionEnd: -1,
michael@0 218 textBeforeCursor: "",
michael@0 219 textAfterCursor: "",
michael@0 220 scrollIntoViewTimeout: null,
michael@0 221 _focusedElement: null,
michael@0 222 _focusCounter: 0, // up one for every time we focus a new element
michael@0 223 _observer: null,
michael@0 224 _documentEncoder: null,
michael@0 225 _editor: null,
michael@0 226 _editing: false,
michael@0 227
michael@0 228 get focusedElement() {
michael@0 229 if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
michael@0 230 this._focusedElement = null;
michael@0 231
michael@0 232 return this._focusedElement;
michael@0 233 },
michael@0 234
michael@0 235 set focusedElement(val) {
michael@0 236 this._focusCounter++;
michael@0 237 this._focusedElement = val;
michael@0 238 },
michael@0 239
michael@0 240 setFocusedElement: function fa_setFocusedElement(element) {
michael@0 241 let self = this;
michael@0 242
michael@0 243 if (element === this.focusedElement)
michael@0 244 return;
michael@0 245
michael@0 246 if (this.focusedElement) {
michael@0 247 this.focusedElement.removeEventListener('mousedown', this);
michael@0 248 this.focusedElement.removeEventListener('mouseup', this);
michael@0 249 this.focusedElement.removeEventListener('compositionend', this);
michael@0 250 if (this._observer) {
michael@0 251 this._observer.disconnect();
michael@0 252 this._observer = null;
michael@0 253 }
michael@0 254 if (!element) {
michael@0 255 this.focusedElement.blur();
michael@0 256 }
michael@0 257 }
michael@0 258
michael@0 259 this._documentEncoder = null;
michael@0 260 if (this._editor) {
michael@0 261 // When the nsIFrame of the input element is reconstructed by
michael@0 262 // CSS restyling, the editor observers are removed. Catch
michael@0 263 // [nsIEditor.removeEditorObserver] failure exception if that
michael@0 264 // happens.
michael@0 265 try {
michael@0 266 this._editor.removeEditorObserver(this);
michael@0 267 } catch (e) {}
michael@0 268 this._editor = null;
michael@0 269 }
michael@0 270
michael@0 271 if (element) {
michael@0 272 element.addEventListener('mousedown', this);
michael@0 273 element.addEventListener('mouseup', this);
michael@0 274 element.addEventListener('compositionend', this);
michael@0 275 if (isContentEditable(element)) {
michael@0 276 this._documentEncoder = getDocumentEncoder(element);
michael@0 277 }
michael@0 278 this._editor = getPlaintextEditor(element);
michael@0 279 if (this._editor) {
michael@0 280 // Add a nsIEditorObserver to monitor the text content of the focused
michael@0 281 // element.
michael@0 282 this._editor.addEditorObserver(this);
michael@0 283 }
michael@0 284
michael@0 285 // If our focusedElement is removed from DOM we want to handle it properly
michael@0 286 let MutationObserver = element.ownerDocument.defaultView.MutationObserver;
michael@0 287 this._observer = new MutationObserver(function(mutations) {
michael@0 288 var del = [].some.call(mutations, function(m) {
michael@0 289 return [].some.call(m.removedNodes, function(n) {
michael@0 290 return n.contains(element);
michael@0 291 });
michael@0 292 });
michael@0 293 if (del && element === self.focusedElement) {
michael@0 294 // item was deleted, fake a blur so all state gets set correctly
michael@0 295 self.handleEvent({ target: element, type: "blur" });
michael@0 296 }
michael@0 297 });
michael@0 298
michael@0 299 this._observer.observe(element.ownerDocument.body, {
michael@0 300 childList: true,
michael@0 301 subtree: true
michael@0 302 });
michael@0 303 }
michael@0 304
michael@0 305 this.focusedElement = element;
michael@0 306 },
michael@0 307
michael@0 308 get documentEncoder() {
michael@0 309 return this._documentEncoder;
michael@0 310 },
michael@0 311
michael@0 312 // Get the nsIPlaintextEditor object of current input field.
michael@0 313 get editor() {
michael@0 314 return this._editor;
michael@0 315 },
michael@0 316
michael@0 317 // Implements nsIEditorObserver get notification when the text content of
michael@0 318 // current input field has changed.
michael@0 319 EditAction: function fa_editAction() {
michael@0 320 if (this._editing) {
michael@0 321 return;
michael@0 322 }
michael@0 323 this.sendKeyboardState(this.focusedElement);
michael@0 324 },
michael@0 325
michael@0 326 handleEvent: function fa_handleEvent(evt) {
michael@0 327 let target = evt.target;
michael@0 328
michael@0 329 let range = null;
michael@0 330 switch (evt.type) {
michael@0 331 case "focus":
michael@0 332 if (!target) {
michael@0 333 break;
michael@0 334 }
michael@0 335
michael@0 336 // Focusing on Window, Document or iFrame should focus body
michael@0 337 if (target instanceof HTMLHtmlElement) {
michael@0 338 target = target.document.body;
michael@0 339 } else if (target instanceof HTMLDocument) {
michael@0 340 target = target.body;
michael@0 341 } else if (target instanceof HTMLIFrameElement) {
michael@0 342 target = target.contentDocument ? target.contentDocument.body
michael@0 343 : null;
michael@0 344 }
michael@0 345
michael@0 346 if (!target) {
michael@0 347 break;
michael@0 348 }
michael@0 349
michael@0 350 if (isContentEditable(target)) {
michael@0 351 this.showKeyboard(this.getTopLevelEditable(target));
michael@0 352 this.updateSelection();
michael@0 353 break;
michael@0 354 }
michael@0 355
michael@0 356 if (this.isFocusableElement(target)) {
michael@0 357 this.showKeyboard(target);
michael@0 358 this.updateSelection();
michael@0 359 }
michael@0 360 break;
michael@0 361
michael@0 362 case "pagehide":
michael@0 363 case "beforeunload":
michael@0 364 // We are only interested to the pagehide and beforeunload events from
michael@0 365 // the root document.
michael@0 366 if (target && target != content.document) {
michael@0 367 break;
michael@0 368 }
michael@0 369 // fall through
michael@0 370 case "blur":
michael@0 371 case "submit":
michael@0 372 if (this.focusedElement) {
michael@0 373 this.hideKeyboard();
michael@0 374 this.selectionStart = -1;
michael@0 375 this.selectionEnd = -1;
michael@0 376 }
michael@0 377 break;
michael@0 378
michael@0 379 case 'mousedown':
michael@0 380 if (!this.focusedElement) {
michael@0 381 break;
michael@0 382 }
michael@0 383
michael@0 384 // We only listen for this event on the currently focused element.
michael@0 385 // When the mouse goes down, note the cursor/selection position
michael@0 386 this.updateSelection();
michael@0 387 break;
michael@0 388
michael@0 389 case 'mouseup':
michael@0 390 if (!this.focusedElement) {
michael@0 391 break;
michael@0 392 }
michael@0 393
michael@0 394 // We only listen for this event on the currently focused element.
michael@0 395 // When the mouse goes up, see if the cursor has moved (or the
michael@0 396 // selection changed) since the mouse went down. If it has, we
michael@0 397 // need to tell the keyboard about it
michael@0 398 range = getSelectionRange(this.focusedElement);
michael@0 399 if (range[0] !== this.selectionStart ||
michael@0 400 range[1] !== this.selectionEnd) {
michael@0 401 this.updateSelection();
michael@0 402 }
michael@0 403 break;
michael@0 404
michael@0 405 case "resize":
michael@0 406 if (!this.isKeyboardOpened)
michael@0 407 return;
michael@0 408
michael@0 409 if (this.scrollIntoViewTimeout) {
michael@0 410 content.clearTimeout(this.scrollIntoViewTimeout);
michael@0 411 this.scrollIntoViewTimeout = null;
michael@0 412 }
michael@0 413
michael@0 414 // We may receive multiple resize events in quick succession, so wait
michael@0 415 // a bit before scrolling the input element into view.
michael@0 416 if (this.focusedElement) {
michael@0 417 this.scrollIntoViewTimeout = content.setTimeout(function () {
michael@0 418 this.scrollIntoViewTimeout = null;
michael@0 419 if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
michael@0 420 scrollSelectionOrElementIntoView(this.focusedElement);
michael@0 421 }
michael@0 422 }.bind(this), RESIZE_SCROLL_DELAY);
michael@0 423 }
michael@0 424 break;
michael@0 425
michael@0 426 case "input":
michael@0 427 if (this.focusedElement) {
michael@0 428 // When the text content changes, notify the keyboard
michael@0 429 this.updateSelection();
michael@0 430 }
michael@0 431 break;
michael@0 432
michael@0 433 case "keydown":
michael@0 434 if (!this.focusedElement) {
michael@0 435 break;
michael@0 436 }
michael@0 437
michael@0 438 CompositionManager.endComposition('');
michael@0 439
michael@0 440 // We use 'setTimeout' to wait until the input element accomplishes the
michael@0 441 // change in selection range.
michael@0 442 content.setTimeout(function() {
michael@0 443 this.updateSelection();
michael@0 444 }.bind(this), 0);
michael@0 445 break;
michael@0 446
michael@0 447 case "keyup":
michael@0 448 if (!this.focusedElement) {
michael@0 449 break;
michael@0 450 }
michael@0 451
michael@0 452 CompositionManager.endComposition('');
michael@0 453
michael@0 454 break;
michael@0 455
michael@0 456 case "compositionend":
michael@0 457 if (!this.focusedElement) {
michael@0 458 break;
michael@0 459 }
michael@0 460
michael@0 461 CompositionManager.onCompositionEnd();
michael@0 462 break;
michael@0 463 }
michael@0 464 },
michael@0 465
michael@0 466 receiveMessage: function fa_receiveMessage(msg) {
michael@0 467 let target = this.focusedElement;
michael@0 468 let json = msg.json;
michael@0 469
michael@0 470 // To not break mozKeyboard contextId is optional
michael@0 471 if ('contextId' in json &&
michael@0 472 json.contextId !== this._focusCounter &&
michael@0 473 json.requestId) {
michael@0 474 // Ignore messages that are meant for a previously focused element
michael@0 475 sendAsyncMessage("Forms:SequenceError", {
michael@0 476 requestId: json.requestId,
michael@0 477 error: "Expected contextId " + this._focusCounter +
michael@0 478 " but was " + json.contextId
michael@0 479 });
michael@0 480 return;
michael@0 481 }
michael@0 482
michael@0 483 if (!target) {
michael@0 484 switch (msg.name) {
michael@0 485 case "Forms:GetText":
michael@0 486 sendAsyncMessage("Forms:GetText:Result:Error", {
michael@0 487 requestId: json.requestId,
michael@0 488 error: "No focused element"
michael@0 489 });
michael@0 490 break;
michael@0 491 }
michael@0 492 return;
michael@0 493 }
michael@0 494
michael@0 495 this._editing = true;
michael@0 496 switch (msg.name) {
michael@0 497 case "Forms:Input:Value": {
michael@0 498 CompositionManager.endComposition('');
michael@0 499
michael@0 500 target.value = json.value;
michael@0 501
michael@0 502 let event = target.ownerDocument.createEvent('HTMLEvents');
michael@0 503 event.initEvent('input', true, false);
michael@0 504 target.dispatchEvent(event);
michael@0 505 break;
michael@0 506 }
michael@0 507
michael@0 508 case "Forms:Input:SendKey":
michael@0 509 CompositionManager.endComposition('');
michael@0 510
michael@0 511 this._editing = true;
michael@0 512 let doKeypress = domWindowUtils.sendKeyEvent('keydown', json.keyCode,
michael@0 513 json.charCode, json.modifiers);
michael@0 514 if (doKeypress) {
michael@0 515 domWindowUtils.sendKeyEvent('keypress', json.keyCode,
michael@0 516 json.charCode, json.modifiers);
michael@0 517 }
michael@0 518
michael@0 519 if(!json.repeat) {
michael@0 520 domWindowUtils.sendKeyEvent('keyup', json.keyCode,
michael@0 521 json.charCode, json.modifiers);
michael@0 522 }
michael@0 523
michael@0 524 this._editing = false;
michael@0 525
michael@0 526 if (json.requestId && doKeypress) {
michael@0 527 sendAsyncMessage("Forms:SendKey:Result:OK", {
michael@0 528 requestId: json.requestId
michael@0 529 });
michael@0 530 }
michael@0 531 else if (json.requestId && !doKeypress) {
michael@0 532 sendAsyncMessage("Forms:SendKey:Result:Error", {
michael@0 533 requestId: json.requestId,
michael@0 534 error: "Keydown event got canceled"
michael@0 535 });
michael@0 536 }
michael@0 537 break;
michael@0 538
michael@0 539 case "Forms:Select:Choice":
michael@0 540 let options = target.options;
michael@0 541 let valueChanged = false;
michael@0 542 if ("index" in json) {
michael@0 543 if (options.selectedIndex != json.index) {
michael@0 544 options.selectedIndex = json.index;
michael@0 545 valueChanged = true;
michael@0 546 }
michael@0 547 } else if ("indexes" in json) {
michael@0 548 for (let i = 0; i < options.length; i++) {
michael@0 549 let newValue = (json.indexes.indexOf(i) != -1);
michael@0 550 if (options.item(i).selected != newValue) {
michael@0 551 options.item(i).selected = newValue;
michael@0 552 valueChanged = true;
michael@0 553 }
michael@0 554 }
michael@0 555 }
michael@0 556
michael@0 557 // only fire onchange event if any selected option is changed
michael@0 558 if (valueChanged) {
michael@0 559 let event = target.ownerDocument.createEvent('HTMLEvents');
michael@0 560 event.initEvent('change', true, true);
michael@0 561 target.dispatchEvent(event);
michael@0 562 }
michael@0 563 break;
michael@0 564
michael@0 565 case "Forms:Select:Blur": {
michael@0 566 this.setFocusedElement(null);
michael@0 567 break;
michael@0 568 }
michael@0 569
michael@0 570 case "Forms:SetSelectionRange": {
michael@0 571 CompositionManager.endComposition('');
michael@0 572
michael@0 573 let start = json.selectionStart;
michael@0 574 let end = json.selectionEnd;
michael@0 575
michael@0 576 if (!setSelectionRange(target, start, end)) {
michael@0 577 if (json.requestId) {
michael@0 578 sendAsyncMessage("Forms:SetSelectionRange:Result:Error", {
michael@0 579 requestId: json.requestId,
michael@0 580 error: "failed"
michael@0 581 });
michael@0 582 }
michael@0 583 break;
michael@0 584 }
michael@0 585
michael@0 586 this.updateSelection();
michael@0 587
michael@0 588 if (json.requestId) {
michael@0 589 sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
michael@0 590 requestId: json.requestId,
michael@0 591 selectioninfo: this.getSelectionInfo()
michael@0 592 });
michael@0 593 }
michael@0 594 break;
michael@0 595 }
michael@0 596
michael@0 597 case "Forms:ReplaceSurroundingText": {
michael@0 598 CompositionManager.endComposition('');
michael@0 599
michael@0 600 let selectionRange = getSelectionRange(target);
michael@0 601 if (!replaceSurroundingText(target,
michael@0 602 json.text,
michael@0 603 selectionRange[0],
michael@0 604 selectionRange[1],
michael@0 605 json.offset,
michael@0 606 json.length)) {
michael@0 607 if (json.requestId) {
michael@0 608 sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", {
michael@0 609 requestId: json.requestId,
michael@0 610 error: "failed"
michael@0 611 });
michael@0 612 }
michael@0 613 break;
michael@0 614 }
michael@0 615
michael@0 616 if (json.requestId) {
michael@0 617 sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
michael@0 618 requestId: json.requestId,
michael@0 619 selectioninfo: this.getSelectionInfo()
michael@0 620 });
michael@0 621 }
michael@0 622 break;
michael@0 623 }
michael@0 624
michael@0 625 case "Forms:GetText": {
michael@0 626 let value = isContentEditable(target) ? getContentEditableText(target)
michael@0 627 : target.value;
michael@0 628
michael@0 629 if (json.offset && json.length) {
michael@0 630 value = value.substr(json.offset, json.length);
michael@0 631 }
michael@0 632 else if (json.offset) {
michael@0 633 value = value.substr(json.offset);
michael@0 634 }
michael@0 635
michael@0 636 sendAsyncMessage("Forms:GetText:Result:OK", {
michael@0 637 requestId: json.requestId,
michael@0 638 text: value
michael@0 639 });
michael@0 640 break;
michael@0 641 }
michael@0 642
michael@0 643 case "Forms:GetContext": {
michael@0 644 let obj = getJSON(target, this._focusCounter);
michael@0 645 sendAsyncMessage("Forms:GetContext:Result:OK", obj);
michael@0 646 break;
michael@0 647 }
michael@0 648
michael@0 649 case "Forms:SetComposition": {
michael@0 650 CompositionManager.setComposition(target, json.text, json.cursor,
michael@0 651 json.clauses);
michael@0 652 sendAsyncMessage("Forms:SetComposition:Result:OK", {
michael@0 653 requestId: json.requestId,
michael@0 654 });
michael@0 655 break;
michael@0 656 }
michael@0 657
michael@0 658 case "Forms:EndComposition": {
michael@0 659 CompositionManager.endComposition(json.text);
michael@0 660 sendAsyncMessage("Forms:EndComposition:Result:OK", {
michael@0 661 requestId: json.requestId,
michael@0 662 });
michael@0 663 break;
michael@0 664 }
michael@0 665 }
michael@0 666 this._editing = false;
michael@0 667
michael@0 668 },
michael@0 669
michael@0 670 showKeyboard: function fa_showKeyboard(target) {
michael@0 671 if (this.focusedElement === target)
michael@0 672 return;
michael@0 673
michael@0 674 if (target instanceof HTMLOptionElement)
michael@0 675 target = target.parentNode;
michael@0 676
michael@0 677 this.setFocusedElement(target);
michael@0 678
michael@0 679 let kbOpened = this.sendKeyboardState(target);
michael@0 680 if (this.isTextInputElement(target))
michael@0 681 this.isKeyboardOpened = kbOpened;
michael@0 682 },
michael@0 683
michael@0 684 hideKeyboard: function fa_hideKeyboard() {
michael@0 685 sendAsyncMessage("Forms:Input", { "type": "blur" });
michael@0 686 this.isKeyboardOpened = false;
michael@0 687 this.setFocusedElement(null);
michael@0 688 },
michael@0 689
michael@0 690 isFocusableElement: function fa_isFocusableElement(element) {
michael@0 691 if (element instanceof HTMLSelectElement ||
michael@0 692 element instanceof HTMLTextAreaElement)
michael@0 693 return true;
michael@0 694
michael@0 695 if (element instanceof HTMLOptionElement &&
michael@0 696 element.parentNode instanceof HTMLSelectElement)
michael@0 697 return true;
michael@0 698
michael@0 699 return (element instanceof HTMLInputElement &&
michael@0 700 !this.ignoredInputTypes.has(element.type));
michael@0 701 },
michael@0 702
michael@0 703 isTextInputElement: function fa_isTextInputElement(element) {
michael@0 704 return element instanceof HTMLInputElement ||
michael@0 705 element instanceof HTMLTextAreaElement ||
michael@0 706 isContentEditable(element);
michael@0 707 },
michael@0 708
michael@0 709 getTopLevelEditable: function fa_getTopLevelEditable(element) {
michael@0 710 function retrieveTopLevelEditable(element) {
michael@0 711 while (element && !isContentEditable(element))
michael@0 712 element = element.parentNode;
michael@0 713
michael@0 714 return element;
michael@0 715 }
michael@0 716
michael@0 717 return retrieveTopLevelEditable(element) || element;
michael@0 718 },
michael@0 719
michael@0 720 sendKeyboardState: function(element) {
michael@0 721 // FIXME/bug 729623: work around apparent bug in the IME manager
michael@0 722 // in gecko.
michael@0 723 let readonly = element.getAttribute("readonly");
michael@0 724 if (readonly) {
michael@0 725 return false;
michael@0 726 }
michael@0 727
michael@0 728 sendAsyncMessage("Forms:Input", getJSON(element, this._focusCounter));
michael@0 729 return true;
michael@0 730 },
michael@0 731
michael@0 732 getSelectionInfo: function fa_getSelectionInfo() {
michael@0 733 let element = this.focusedElement;
michael@0 734 let range = getSelectionRange(element);
michael@0 735
michael@0 736 let text = isContentEditable(element) ? getContentEditableText(element)
michael@0 737 : element.value;
michael@0 738
michael@0 739 let textAround = getTextAroundCursor(text, range);
michael@0 740
michael@0 741 let changed = this.selectionStart !== range[0] ||
michael@0 742 this.selectionEnd !== range[1] ||
michael@0 743 this.textBeforeCursor !== textAround.before ||
michael@0 744 this.textAfterCursor !== textAround.after;
michael@0 745
michael@0 746 this.selectionStart = range[0];
michael@0 747 this.selectionEnd = range[1];
michael@0 748 this.textBeforeCursor = textAround.before;
michael@0 749 this.textAfterCursor = textAround.after;
michael@0 750
michael@0 751 return {
michael@0 752 selectionStart: range[0],
michael@0 753 selectionEnd: range[1],
michael@0 754 textBeforeCursor: textAround.before,
michael@0 755 textAfterCursor: textAround.after,
michael@0 756 changed: changed
michael@0 757 };
michael@0 758 },
michael@0 759
michael@0 760 // Notify when the selection range changes
michael@0 761 updateSelection: function fa_updateSelection() {
michael@0 762 if (!this.focusedElement) {
michael@0 763 return;
michael@0 764 }
michael@0 765 let selectionInfo = this.getSelectionInfo();
michael@0 766 if (selectionInfo.changed) {
michael@0 767 sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
michael@0 768 }
michael@0 769 }
michael@0 770 };
michael@0 771
michael@0 772 FormAssistant.init();
michael@0 773
michael@0 774 function isContentEditable(element) {
michael@0 775 if (!element) {
michael@0 776 return false;
michael@0 777 }
michael@0 778
michael@0 779 if (element.isContentEditable || element.designMode == "on")
michael@0 780 return true;
michael@0 781
michael@0 782 return element.ownerDocument && element.ownerDocument.designMode == "on";
michael@0 783 }
michael@0 784
michael@0 785 function isPlainTextField(element) {
michael@0 786 if (!element) {
michael@0 787 return false;
michael@0 788 }
michael@0 789
michael@0 790 return element instanceof HTMLTextAreaElement ||
michael@0 791 (element instanceof HTMLInputElement &&
michael@0 792 element.mozIsTextField(false));
michael@0 793 }
michael@0 794
michael@0 795 function getJSON(element, focusCounter) {
michael@0 796 // <input type=number> has a nested anonymous <input type=text> element that
michael@0 797 // takes focus on behalf of the number control when someone tries to focus
michael@0 798 // the number control. If |element| is such an anonymous text control then we
michael@0 799 // need it's number control here in order to get the correct 'type' etc.:
michael@0 800 element = element.ownerNumberControl || element;
michael@0 801
michael@0 802 let type = element.type || "";
michael@0 803 let value = element.value || "";
michael@0 804 let max = element.max || "";
michael@0 805 let min = element.min || "";
michael@0 806
michael@0 807 // Treat contenteditble element as a special text area field
michael@0 808 if (isContentEditable(element)) {
michael@0 809 type = "textarea";
michael@0 810 value = getContentEditableText(element);
michael@0 811 }
michael@0 812
michael@0 813 // Until the input type=date/datetime/range have been implemented
michael@0 814 // let's return their real type even if the platform returns 'text'
michael@0 815 let attributeType = element.getAttribute("type") || "";
michael@0 816
michael@0 817 if (attributeType) {
michael@0 818 var typeLowerCase = attributeType.toLowerCase();
michael@0 819 switch (typeLowerCase) {
michael@0 820 case "datetime":
michael@0 821 case "datetime-local":
michael@0 822 case "range":
michael@0 823 type = typeLowerCase;
michael@0 824 break;
michael@0 825 }
michael@0 826 }
michael@0 827
michael@0 828 // Gecko has some support for @inputmode but behind a preference and
michael@0 829 // it is disabled by default.
michael@0 830 // Gaia is then using @x-inputmode has its proprietary way to set
michael@0 831 // inputmode for fields. This shouldn't be used outside of pre-installed
michael@0 832 // apps because the attribute is going to disappear as soon as a definitive
michael@0 833 // solution will be find.
michael@0 834 let inputmode = element.getAttribute('x-inputmode');
michael@0 835 if (inputmode) {
michael@0 836 inputmode = inputmode.toLowerCase();
michael@0 837 } else {
michael@0 838 inputmode = '';
michael@0 839 }
michael@0 840
michael@0 841 let range = getSelectionRange(element);
michael@0 842 let textAround = getTextAroundCursor(value, range);
michael@0 843
michael@0 844 return {
michael@0 845 "contextId": focusCounter,
michael@0 846
michael@0 847 "type": type.toLowerCase(),
michael@0 848 "choices": getListForElement(element),
michael@0 849 "value": value,
michael@0 850 "inputmode": inputmode,
michael@0 851 "selectionStart": range[0],
michael@0 852 "selectionEnd": range[1],
michael@0 853 "max": max,
michael@0 854 "min": min,
michael@0 855 "lang": element.lang || "",
michael@0 856 "textBeforeCursor": textAround.before,
michael@0 857 "textAfterCursor": textAround.after
michael@0 858 };
michael@0 859 }
michael@0 860
michael@0 861 function getTextAroundCursor(value, range) {
michael@0 862 let textBeforeCursor = range[0] < 100 ?
michael@0 863 value.substr(0, range[0]) :
michael@0 864 value.substr(range[0] - 100, 100);
michael@0 865
michael@0 866 let textAfterCursor = range[1] + 100 > value.length ?
michael@0 867 value.substr(range[0], value.length) :
michael@0 868 value.substr(range[0], range[1] - range[0] + 100);
michael@0 869
michael@0 870 return {
michael@0 871 before: textBeforeCursor,
michael@0 872 after: textAfterCursor
michael@0 873 };
michael@0 874 }
michael@0 875
michael@0 876 function getListForElement(element) {
michael@0 877 if (!(element instanceof HTMLSelectElement))
michael@0 878 return null;
michael@0 879
michael@0 880 let optionIndex = 0;
michael@0 881 let result = {
michael@0 882 "multiple": element.multiple,
michael@0 883 "choices": []
michael@0 884 };
michael@0 885
michael@0 886 // Build up a flat JSON array of the choices.
michael@0 887 // In HTML, it's possible for select element choices to be under a
michael@0 888 // group header (but not recursively). We distinguish between headers
michael@0 889 // and entries using the boolean "list.group".
michael@0 890 let children = element.children;
michael@0 891 for (let i = 0; i < children.length; i++) {
michael@0 892 let child = children[i];
michael@0 893
michael@0 894 if (child instanceof HTMLOptGroupElement) {
michael@0 895 result.choices.push({
michael@0 896 "group": true,
michael@0 897 "text": child.label || child.firstChild.data,
michael@0 898 "disabled": child.disabled
michael@0 899 });
michael@0 900
michael@0 901 let subchildren = child.children;
michael@0 902 for (let j = 0; j < subchildren.length; j++) {
michael@0 903 let subchild = subchildren[j];
michael@0 904 result.choices.push({
michael@0 905 "group": false,
michael@0 906 "inGroup": true,
michael@0 907 "text": subchild.text,
michael@0 908 "disabled": child.disabled || subchild.disabled,
michael@0 909 "selected": subchild.selected,
michael@0 910 "optionIndex": optionIndex++
michael@0 911 });
michael@0 912 }
michael@0 913 } else if (child instanceof HTMLOptionElement) {
michael@0 914 result.choices.push({
michael@0 915 "group": false,
michael@0 916 "inGroup": false,
michael@0 917 "text": child.text,
michael@0 918 "disabled": child.disabled,
michael@0 919 "selected": child.selected,
michael@0 920 "optionIndex": optionIndex++
michael@0 921 });
michael@0 922 }
michael@0 923 }
michael@0 924
michael@0 925 return result;
michael@0 926 };
michael@0 927
michael@0 928 // Create a plain text document encode from the focused element.
michael@0 929 function getDocumentEncoder(element) {
michael@0 930 let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
michael@0 931 .createInstance(Ci.nsIDocumentEncoder);
michael@0 932 let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent |
michael@0 933 Ci.nsIDocumentEncoder.OutputRaw |
michael@0 934 Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
michael@0 935 // Bug 902847. Don't trim trailing spaces of a line.
michael@0 936 Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces |
michael@0 937 Ci.nsIDocumentEncoder.OutputLFLineBreak |
michael@0 938 Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder;
michael@0 939 encoder.init(element.ownerDocument, "text/plain", flags);
michael@0 940 return encoder;
michael@0 941 }
michael@0 942
michael@0 943 // Get the visible content text of a content editable element
michael@0 944 function getContentEditableText(element) {
michael@0 945 if (!element || !isContentEditable(element)) {
michael@0 946 return null;
michael@0 947 }
michael@0 948
michael@0 949 let doc = element.ownerDocument;
michael@0 950 let range = doc.createRange();
michael@0 951 range.selectNodeContents(element);
michael@0 952 let encoder = FormAssistant.documentEncoder;
michael@0 953 encoder.setRange(range);
michael@0 954 return encoder.encodeToString();
michael@0 955 }
michael@0 956
michael@0 957 function getSelectionRange(element) {
michael@0 958 let start = 0;
michael@0 959 let end = 0;
michael@0 960 if (isPlainTextField(element)) {
michael@0 961 // Get the selection range of <input> and <textarea> elements
michael@0 962 start = element.selectionStart;
michael@0 963 end = element.selectionEnd;
michael@0 964 } else if (isContentEditable(element)){
michael@0 965 // Get the selection range of contenteditable elements
michael@0 966 let win = element.ownerDocument.defaultView;
michael@0 967 let sel = win.getSelection();
michael@0 968 if (sel && sel.rangeCount > 0) {
michael@0 969 start = getContentEditableSelectionStart(element, sel);
michael@0 970 end = start + getContentEditableSelectionLength(element, sel);
michael@0 971 } else {
michael@0 972 dump("Failed to get window.getSelection()\n");
michael@0 973 }
michael@0 974 }
michael@0 975 return [start, end];
michael@0 976 }
michael@0 977
michael@0 978 function getContentEditableSelectionStart(element, selection) {
michael@0 979 let doc = element.ownerDocument;
michael@0 980 let range = doc.createRange();
michael@0 981 range.setStart(element, 0);
michael@0 982 range.setEnd(selection.anchorNode, selection.anchorOffset);
michael@0 983 let encoder = FormAssistant.documentEncoder;
michael@0 984 encoder.setRange(range);
michael@0 985 return encoder.encodeToString().length;
michael@0 986 }
michael@0 987
michael@0 988 function getContentEditableSelectionLength(element, selection) {
michael@0 989 let encoder = FormAssistant.documentEncoder;
michael@0 990 encoder.setRange(selection.getRangeAt(0));
michael@0 991 return encoder.encodeToString().length;
michael@0 992 }
michael@0 993
michael@0 994 function setSelectionRange(element, start, end) {
michael@0 995 let isTextField = isPlainTextField(element);
michael@0 996
michael@0 997 // Check the parameters
michael@0 998
michael@0 999 if (!isTextField && !isContentEditable(element)) {
michael@0 1000 // Skip HTMLOptionElement and HTMLSelectElement elements, as they don't
michael@0 1001 // support the operation of setSelectionRange
michael@0 1002 return false;
michael@0 1003 }
michael@0 1004
michael@0 1005 let text = isTextField ? element.value : getContentEditableText(element);
michael@0 1006 let length = text.length;
michael@0 1007 if (start < 0) {
michael@0 1008 start = 0;
michael@0 1009 }
michael@0 1010 if (end > length) {
michael@0 1011 end = length;
michael@0 1012 }
michael@0 1013 if (start > end) {
michael@0 1014 start = end;
michael@0 1015 }
michael@0 1016
michael@0 1017 if (isTextField) {
michael@0 1018 // Set the selection range of <input> and <textarea> elements
michael@0 1019 element.setSelectionRange(start, end, "forward");
michael@0 1020 return true;
michael@0 1021 } else {
michael@0 1022 // set the selection range of contenteditable elements
michael@0 1023 let win = element.ownerDocument.defaultView;
michael@0 1024 let sel = win.getSelection();
michael@0 1025
michael@0 1026 // Move the caret to the start position
michael@0 1027 sel.collapse(element, 0);
michael@0 1028 for (let i = 0; i < start; i++) {
michael@0 1029 sel.modify("move", "forward", "character");
michael@0 1030 }
michael@0 1031
michael@0 1032 // Avoid entering infinite loop in case we cannot change the selection
michael@0 1033 // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
michael@0 1034 let oldStart = getContentEditableSelectionStart(element, sel);
michael@0 1035 let counter = 0;
michael@0 1036 while (oldStart < start) {
michael@0 1037 sel.modify("move", "forward", "character");
michael@0 1038 let newStart = getContentEditableSelectionStart(element, sel);
michael@0 1039 if (oldStart == newStart) {
michael@0 1040 counter++;
michael@0 1041 if (counter > MAX_BLOCKED_COUNT) {
michael@0 1042 return false;
michael@0 1043 }
michael@0 1044 } else {
michael@0 1045 counter = 0;
michael@0 1046 oldStart = newStart;
michael@0 1047 }
michael@0 1048 }
michael@0 1049
michael@0 1050 // Extend the selection to the end position
michael@0 1051 for (let i = start; i < end; i++) {
michael@0 1052 sel.modify("extend", "forward", "character");
michael@0 1053 }
michael@0 1054
michael@0 1055 // Avoid entering infinite loop in case we cannot change the selection
michael@0 1056 // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
michael@0 1057 counter = 0;
michael@0 1058 let selectionLength = end - start;
michael@0 1059 let oldSelectionLength = getContentEditableSelectionLength(element, sel);
michael@0 1060 while (oldSelectionLength < selectionLength) {
michael@0 1061 sel.modify("extend", "forward", "character");
michael@0 1062 let newSelectionLength = getContentEditableSelectionLength(element, sel);
michael@0 1063 if (oldSelectionLength == newSelectionLength ) {
michael@0 1064 counter++;
michael@0 1065 if (counter > MAX_BLOCKED_COUNT) {
michael@0 1066 return false;
michael@0 1067 }
michael@0 1068 } else {
michael@0 1069 counter = 0;
michael@0 1070 oldSelectionLength = newSelectionLength;
michael@0 1071 }
michael@0 1072 }
michael@0 1073 return true;
michael@0 1074 }
michael@0 1075 }
michael@0 1076
michael@0 1077 /**
michael@0 1078 * Scroll the given element into view.
michael@0 1079 *
michael@0 1080 * Calls scrollSelectionIntoView for contentEditable elements.
michael@0 1081 */
michael@0 1082 function scrollSelectionOrElementIntoView(element) {
michael@0 1083 let editor = getPlaintextEditor(element);
michael@0 1084 if (editor) {
michael@0 1085 editor.selectionController.scrollSelectionIntoView(
michael@0 1086 Ci.nsISelectionController.SELECTION_NORMAL,
michael@0 1087 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
michael@0 1088 Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
michael@0 1089 } else {
michael@0 1090 element.scrollIntoView(false);
michael@0 1091 }
michael@0 1092 }
michael@0 1093
michael@0 1094 // Get nsIPlaintextEditor object from an input field
michael@0 1095 function getPlaintextEditor(element) {
michael@0 1096 let editor = null;
michael@0 1097 // Get nsIEditor
michael@0 1098 if (isPlainTextField(element)) {
michael@0 1099 // Get from the <input> and <textarea> elements
michael@0 1100 editor = element.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
michael@0 1101 } else if (isContentEditable(element)) {
michael@0 1102 // Get from content editable element
michael@0 1103 let win = element.ownerDocument.defaultView;
michael@0 1104 let editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1105 .getInterface(Ci.nsIWebNavigation)
michael@0 1106 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1107 .getInterface(Ci.nsIEditingSession);
michael@0 1108 if (editingSession) {
michael@0 1109 editor = editingSession.getEditorForWindow(win);
michael@0 1110 }
michael@0 1111 }
michael@0 1112 if (editor) {
michael@0 1113 editor.QueryInterface(Ci.nsIPlaintextEditor);
michael@0 1114 }
michael@0 1115 return editor;
michael@0 1116 }
michael@0 1117
michael@0 1118 function replaceSurroundingText(element, text, selectionStart, selectionEnd,
michael@0 1119 offset, length) {
michael@0 1120 let editor = FormAssistant.editor;
michael@0 1121 if (!editor) {
michael@0 1122 return false;
michael@0 1123 }
michael@0 1124
michael@0 1125 // Check the parameters.
michael@0 1126 let start = selectionStart + offset;
michael@0 1127 if (start < 0) {
michael@0 1128 start = 0;
michael@0 1129 }
michael@0 1130 if (length < 0) {
michael@0 1131 length = 0;
michael@0 1132 }
michael@0 1133 let end = start + length;
michael@0 1134
michael@0 1135 if (selectionStart != start || selectionEnd != end) {
michael@0 1136 // Change selection range before replacing.
michael@0 1137 if (!setSelectionRange(element, start, end)) {
michael@0 1138 return false;
michael@0 1139 }
michael@0 1140 }
michael@0 1141
michael@0 1142 if (start != end) {
michael@0 1143 // Delete the selected text.
michael@0 1144 editor.deleteSelection(Ci.nsIEditor.ePrevious, Ci.nsIEditor.eStrip);
michael@0 1145 }
michael@0 1146
michael@0 1147 if (text) {
michael@0 1148 // We don't use CR but LF
michael@0 1149 // see https://bugzilla.mozilla.org/show_bug.cgi?id=902847
michael@0 1150 text = text.replace(/\r/g, '\n');
michael@0 1151 // Insert the text to be replaced with.
michael@0 1152 editor.insertText(text);
michael@0 1153 }
michael@0 1154 return true;
michael@0 1155 }
michael@0 1156
michael@0 1157 let CompositionManager = {
michael@0 1158 _isStarted: false,
michael@0 1159 _text: '',
michael@0 1160 _clauseAttrMap: {
michael@0 1161 'raw-input':
michael@0 1162 Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT,
michael@0 1163 'selected-raw-text':
michael@0 1164 Ci.nsICompositionStringSynthesizer.ATTR_SELECTEDRAWTEXT,
michael@0 1165 'converted-text':
michael@0 1166 Ci.nsICompositionStringSynthesizer.ATTR_CONVERTEDTEXT,
michael@0 1167 'selected-converted-text':
michael@0 1168 Ci.nsICompositionStringSynthesizer.ATTR_SELECTEDCONVERTEDTEXT
michael@0 1169 },
michael@0 1170
michael@0 1171 setComposition: function cm_setComposition(element, text, cursor, clauses) {
michael@0 1172 // Check parameters.
michael@0 1173 if (!element) {
michael@0 1174 return;
michael@0 1175 }
michael@0 1176 let len = text.length;
michael@0 1177 if (cursor > len) {
michael@0 1178 cursor = len;
michael@0 1179 }
michael@0 1180 let clauseLens = [];
michael@0 1181 let clauseAttrs = [];
michael@0 1182 if (clauses) {
michael@0 1183 let remainingLength = len;
michael@0 1184 for (let i = 0; i < clauses.length; i++) {
michael@0 1185 if (clauses[i]) {
michael@0 1186 let clauseLength = clauses[i].length || 0;
michael@0 1187 // Make sure the total clauses length is not bigger than that of the
michael@0 1188 // composition string.
michael@0 1189 if (clauseLength > remainingLength) {
michael@0 1190 clauseLength = remainingLength;
michael@0 1191 }
michael@0 1192 remainingLength -= clauseLength;
michael@0 1193 clauseLens.push(clauseLength);
michael@0 1194 clauseAttrs.push(this._clauseAttrMap[clauses[i].selectionType] ||
michael@0 1195 Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT);
michael@0 1196 }
michael@0 1197 }
michael@0 1198 // If the total clauses length is less than that of the composition
michael@0 1199 // string, extend the last clause to the end of the composition string.
michael@0 1200 if (remainingLength > 0) {
michael@0 1201 clauseLens[clauseLens.length - 1] += remainingLength;
michael@0 1202 }
michael@0 1203 } else {
michael@0 1204 clauseLens.push(len);
michael@0 1205 clauseAttrs.push(Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT);
michael@0 1206 }
michael@0 1207
michael@0 1208 // Start composition if need to.
michael@0 1209 if (!this._isStarted) {
michael@0 1210 this._isStarted = true;
michael@0 1211 domWindowUtils.sendCompositionEvent('compositionstart', '', '');
michael@0 1212 this._text = '';
michael@0 1213 }
michael@0 1214
michael@0 1215 // Update the composing text.
michael@0 1216 if (this._text !== text) {
michael@0 1217 this._text = text;
michael@0 1218 domWindowUtils.sendCompositionEvent('compositionupdate', text, '');
michael@0 1219 }
michael@0 1220 let compositionString = domWindowUtils.createCompositionStringSynthesizer();
michael@0 1221 compositionString.setString(text);
michael@0 1222 for (var i = 0; i < clauseLens.length; i++) {
michael@0 1223 compositionString.appendClause(clauseLens[i], clauseAttrs[i]);
michael@0 1224 }
michael@0 1225 if (cursor >= 0) {
michael@0 1226 compositionString.setCaret(cursor, 0);
michael@0 1227 }
michael@0 1228 compositionString.dispatchEvent();
michael@0 1229 },
michael@0 1230
michael@0 1231 endComposition: function cm_endComposition(text) {
michael@0 1232 if (!this._isStarted) {
michael@0 1233 return;
michael@0 1234 }
michael@0 1235 // Update the composing text.
michael@0 1236 if (this._text !== text) {
michael@0 1237 domWindowUtils.sendCompositionEvent('compositionupdate', text, '');
michael@0 1238 }
michael@0 1239 let compositionString = domWindowUtils.createCompositionStringSynthesizer();
michael@0 1240 compositionString.setString(text);
michael@0 1241 // Set the cursor position to |text.length| so that the text will be
michael@0 1242 // committed before the cursor position.
michael@0 1243 compositionString.setCaret(text.length, 0);
michael@0 1244 compositionString.dispatchEvent();
michael@0 1245 domWindowUtils.sendCompositionEvent('compositionend', text, '');
michael@0 1246 this._text = '';
michael@0 1247 this._isStarted = false;
michael@0 1248 },
michael@0 1249
michael@0 1250 // Composition ends due to external actions.
michael@0 1251 onCompositionEnd: function cm_onCompositionEnd() {
michael@0 1252 if (!this._isStarted) {
michael@0 1253 return;
michael@0 1254 }
michael@0 1255
michael@0 1256 this._text = '';
michael@0 1257 this._isStarted = false;
michael@0 1258 }
michael@0 1259 };

mercurial