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

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

mercurial