Bug 1234459 - Expose full text in the input box to InputMethod API, r=masayuki, sr=smaug
authorTim Chien <timdream@gmail.com>
Tue, 05 Jan 2016 00:37:00 +0100
changeset 318965 165ea60605989dbf4441fb4131ab10146f525560
parent 318964 e4c738873db5e3dec1b74f2f7ad555c7274ec32b
child 318966 9115adb58910737dcbdca45607b2205aefde2056
push id8951
push userbenj@benj.me
push dateTue, 05 Jan 2016 13:08:54 +0000
reviewersmasayuki, smaug
bugs1234459
milestone46.0a1
Bug 1234459 - Expose full text in the input box to InputMethod API, r=masayuki, sr=smaug
dom/inputmethod/Keyboard.jsm
dom/inputmethod/MozKeyboard.js
dom/inputmethod/forms.js
dom/inputmethod/mochitest/test_basic.html
dom/webidl/InputMethod.webidl
--- a/dom/inputmethod/Keyboard.jsm
+++ b/dom/inputmethod/Keyboard.jsm
@@ -52,17 +52,17 @@ this.Keyboard = {
     'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
     'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister'
   ],
 
   _messageNames: [
     'RemoveFocus',
     'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
     'SwitchToNextInputMethod', 'HideInputMethod',
-    'GetText', 'SendKey', 'GetContext',
+    'SendKey', 'GetContext',
     'SetComposition', 'EndComposition',
     'RegisterSync', 'Unregister'
   ],
 
   get formMM() {
     if (this._formMM && !Cu.isDeadWrapper(this._formMM))
       return this._formMM;
 
@@ -157,18 +157,16 @@ this.Keyboard = {
       this.initFormsFrameScript(mm);
     }
   },
 
   initFormsFrameScript: function(mm) {
     mm.addMessageListener('Forms:Focus', this);
     mm.addMessageListener('Forms:Blur', this);
     mm.addMessageListener('Forms:SelectionChange', this);
-    mm.addMessageListener('Forms:GetText:Result:OK', this);
-    mm.addMessageListener('Forms:GetText:Result:Error', this);
     mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
     mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this);
     mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
     mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this);
     mm.addMessageListener('Forms:SendKey:Result:OK', this);
     mm.addMessageListener('Forms:SendKey:Result:Error', this);
     mm.addMessageListener('Forms:SequenceError', this);
     mm.addMessageListener('Forms:GetContext:Result:OK', this);
@@ -221,18 +219,16 @@ this.Keyboard = {
     switch (msg.name) {
       case 'Forms:Focus':
         this.handleFocus(msg);
         break;
       case 'Forms:Blur':
         this.handleBlur(msg);
         break;
       case 'Forms:SelectionChange':
-      case 'Forms:GetText:Result:OK':
-      case 'Forms:GetText:Result:Error':
       case 'Forms:SetSelectionRange:Result:OK':
       case 'Forms:ReplaceSurroundingText:Result:OK':
       case 'Forms:SendKey:Result:OK':
       case 'Forms:SendKey:Result:Error':
       case 'Forms:SequenceError':
       case 'Forms:GetContext:Result:OK':
       case 'Forms:SetComposition:Result:OK':
       case 'Forms:EndComposition:Result:OK':
@@ -283,19 +279,16 @@ this.Keyboard = {
         this.replaceSurroundingText(msg);
         break;
       case 'Keyboard:SwitchToNextInputMethod':
         this.switchToNextInputMethod();
         break;
       case 'Keyboard:ShowInputMethodPicker':
         this.showInputMethodPicker();
         break;
-      case 'Keyboard:GetText':
-        this.getText(msg);
-        break;
       case 'Keyboard:SendKey':
         this.sendKey(msg);
         break;
       case 'Keyboard:GetContext':
         this.getContext(msg);
         break;
       case 'Keyboard:SetComposition':
         this.setComposition(msg);
@@ -418,20 +411,16 @@ this.Keyboard = {
     this.sendToSystem('System:Next', {});
 
     // XXX: To be removed with mozContentEvent support from shell.js
     SystemAppProxy.dispatchEvent({
       type: "inputmethod-next"
     });
   },
 
-  getText: function keyboardGetText(msg) {
-    this.sendToForm('Forms:GetText', msg.data);
-  },
-
   sendKey: function keyboardSendKey(msg) {
     this.sendToForm('Forms:Input:SendKey', msg.data);
   },
 
   getContext: function keyboardGetContext(msg) {
     if (!this.formMM) {
       return;
     }
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -734,34 +734,77 @@ InputContextDOMRequestIpcHelper.prototyp
       dump('InputContextDOMRequestIpcHelper received message without context attached.\n');
       return;
     }
 
     this._inputContext.receiveMessage(msg);
   }
 };
 
+function MozInputContextSelectionChangeEventDetail(ctx, ownAction) {
+  this._ctx = ctx;
+  this.ownAction = ownAction;
+}
+
+MozInputContextSelectionChangeEventDetail.prototype = {
+  classID: Components.ID("ef35443e-a400-4ae3-9170-c2f4e05f7aed"),
+  QueryInterface: XPCOMUtils.generateQI([]),
+
+  ownAction: false,
+
+  get selectionStart() {
+    return this._ctx.selectionStart;
+  },
+
+  get selectionEnd() {
+    return this._ctx.selectionEnd;
+  }
+};
+
+function MozInputContextSurroundingTextChangeEventDetail(ctx, ownAction) {
+  this._ctx = ctx;
+  this.ownAction = ownAction;
+}
+
+MozInputContextSurroundingTextChangeEventDetail.prototype = {
+  classID: Components.ID("1c50fdaf-74af-4b2e-814f-792caf65a168"),
+  QueryInterface: XPCOMUtils.generateQI([]),
+
+  ownAction: false,
+
+  get text() {
+    return this._ctx.text;
+  },
+
+  get textBeforeCursor() {
+    return this._ctx.textBeforeCursor;
+  },
+
+  get textAfterCursor() {
+    return this._ctx.textAfterCursor;
+  }
+};
+
  /**
  * ==============================================
  * InputContext
  * ==============================================
  */
-function MozInputContext(ctx) {
+function MozInputContext(data) {
   this._context = {
-    type: ctx.type,
-    inputType: ctx.inputType,
-    inputMode: ctx.inputMode,
-    lang: ctx.lang,
-    selectionStart: ctx.selectionStart,
-    selectionEnd: ctx.selectionEnd,
-    textBeforeCursor: ctx.textBeforeCursor,
-    textAfterCursor: ctx.textAfterCursor
+    type: data.type,
+    inputType: data.inputType,
+    inputMode: data.inputMode,
+    lang: data.lang,
+    selectionStart: data.selectionStart,
+    selectionEnd: data.selectionEnd,
+    text: data.value
   };
 
-  this._contextId = ctx.contextId;
+  this._contextId = data.contextId;
 }
 
 MozInputContext.prototype = {
   _window: null,
   _context: null,
   _contextId: -1,
   _ipcHelper: null,
 
@@ -844,56 +887,53 @@ MozInputContext.prototype = {
         break;
       default:
         dump("Could not find a handler for " + msg.name);
         resolver.reject();
         break;
     }
   },
 
-  updateSelectionContext: function ic_updateSelectionContext(ctx, ownAction) {
+  updateSelectionContext: function ic_updateSelectionContext(data, ownAction) {
     if (!this._context) {
       return;
     }
 
-    let selectionDirty = this._context.selectionStart !== ctx.selectionStart ||
-          this._context.selectionEnd !== ctx.selectionEnd;
-    let surroundDirty = this._context.textBeforeCursor !== ctx.textBeforeCursor ||
-          this._context.textAfterCursor !== ctx.textAfterCursor;
+    let selectionDirty =
+      this._context.selectionStart !== data.selectionStart ||
+      this._context.selectionEnd !== data.selectionEnd;
+    let surroundDirty = selectionDirty || data.text !== this._contextId.text;
 
-    this._context.selectionStart = ctx.selectionStart;
-    this._context.selectionEnd = ctx.selectionEnd;
-    this._context.textBeforeCursor = ctx.textBeforeCursor;
-    this._context.textAfterCursor = ctx.textAfterCursor;
+    this._context.text = data.text;
+    this._context.selectionStart = data.selectionStart;
+    this._context.selectionEnd = data.selectionEnd;
 
     if (selectionDirty) {
-      this._fireEvent("selectionchange", {
-        selectionStart: ctx.selectionStart,
-        selectionEnd: ctx.selectionEnd,
-        ownAction: ownAction
-      });
+      let selectionChangeDetail =
+        new MozInputContextSelectionChangeEventDetail(this, ownAction);
+      let wrappedSelectionChangeDetail =
+        this._window.MozInputContextSelectionChangeEventDetail
+          ._create(this._window, selectionChangeDetail);
+      let selectionChangeEvent = new this._window.CustomEvent("selectionchange",
+        { cancelable: false, detail: wrappedSelectionChangeDetail });
+
+      this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
     }
 
     if (surroundDirty) {
-      this._fireEvent("surroundingtextchange", {
-        beforeString: ctx.textBeforeCursor,
-        afterString: ctx.textAfterCursor,
-        ownAction: ownAction
-      });
-    }
-  },
+      let surroundingTextChangeDetail =
+        new MozInputContextSurroundingTextChangeEventDetail(this, ownAction);
+      let wrappedSurroundingTextChangeDetail =
+        this._window.MozInputContextSurroundingTextChangeEventDetail
+          ._create(this._window, surroundingTextChangeDetail);
+      let selectionChangeEvent = new this._window.CustomEvent("surroundingtextchange",
+        { cancelable: false, detail: wrappedSurroundingTextChangeDetail });
 
-  _fireEvent: function ic_fireEvent(eventName, aDetail) {
-    let detail = {
-      detail: aDetail
-    };
-
-    let event = new this._window.CustomEvent(eventName,
-                                             Cu.cloneInto(detail, this._window));
-    this.__DOM_IMPL__.dispatchEvent(event);
+      this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
+    }
   },
 
   // tag name of the input field
   get type() {
     return this._context.type;
   },
 
   // type of the input field
@@ -905,41 +945,53 @@ MozInputContext.prototype = {
     return this._context.inputMode;
   },
 
   get lang() {
     return this._context.lang;
   },
 
   getText: function ic_getText(offset, length) {
-    let self = this;
-    return this._sendPromise(function(resolverId) {
-      cpmmSendAsyncMessageWithKbID(self, 'Keyboard:GetText', {
-        contextId: self._contextId,
-        requestId: resolverId,
-        offset: offset,
-        length: length
-      });
-    });
+    let text;
+    if (offset && length) {
+      text = this._context.text.substr(offset, length);
+    } else if (offset) {
+      text = this._context.text.substr(offset);
+    } else {
+      text = this._context.text;
+    }
+
+    return this._window.Promise.resolve(text);
   },
 
   get selectionStart() {
     return this._context.selectionStart;
   },
 
   get selectionEnd() {
     return this._context.selectionEnd;
   },
 
+  get text() {
+    return this._context.text;
+  },
+
   get textBeforeCursor() {
-    return this._context.textBeforeCursor;
+    let text = this._context.text;
+    let start = this._context.selectionStart;
+    return (start < 100) ?
+      text.substr(0, start) :
+      text.substr(start - 100, 100);
   },
 
   get textAfterCursor() {
-    return this._context.textAfterCursor;
+    let text = this._context.text;
+    let start = this._context.selectionStart;
+    let end = this._context.selectionEnd;
+    return text.substr(start, end - start + 100);
   },
 
   setSelectionRange: function ic_setSelectionRange(start, length) {
     let self = this;
     return this._sendPromise(function(resolverId) {
       cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetSelectionRange', {
         contextId: self._contextId,
         requestId: resolverId,
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -418,33 +418,32 @@ var FormAssistant = {
     addEventListener("input", this, true, false);
     addEventListener("keydown", this, true, false);
     addEventListener("keyup", this, true, false);
     addMessageListener("Forms:Select:Choice", this);
     addMessageListener("Forms:Input:Value", this);
     addMessageListener("Forms:Select:Blur", this);
     addMessageListener("Forms:SetSelectionRange", this);
     addMessageListener("Forms:ReplaceSurroundingText", this);
-    addMessageListener("Forms:GetText", this);
     addMessageListener("Forms:Input:SendKey", this);
     addMessageListener("Forms:GetContext", this);
     addMessageListener("Forms:SetComposition", this);
     addMessageListener("Forms:EndComposition", this);
   },
 
   ignoredInputTypes: new Set([
     'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image',
     'range'
   ]),
 
   isHandlingFocus: false,
   selectionStart: -1,
   selectionEnd: -1,
-  textBeforeCursor: "",
-  textAfterCursor: "",
+  text: "",
+
   scrollIntoViewTimeout: null,
   _focusedElement: null,
   _focusCounter: 0, // up one for every time we focus a new element
   _focusDeleteObserver: null,
   _focusContentObserver: null,
   _documentEncoder: null,
   _editor: null,
   _editing: false,
@@ -519,18 +518,16 @@ var FormAssistant = {
       this._focusDeleteObserver = new MutationObserver(function(mutations) {
         var del = [].some.call(mutations, function(m) {
           return [].some.call(m.removedNodes, function(n) {
             return n.contains(element);
           });
         });
         if (del && element === self.focusedElement) {
           self.unhandleFocus();
-          self.selectionStart = -1;
-          self.selectionEnd = -1;
         }
       });
 
       this._focusDeleteObserver.observe(element.ownerDocument.body, {
         childList: true,
         subtree: true
       });
 
@@ -621,18 +618,16 @@ var FormAssistant = {
         if (this.focusedElement && !evt.defaultPrevented) {
           this.focusedElement.blur();
         }
         break;
 
       case "blur":
         if (this.focusedElement) {
           this.unhandleFocus();
-          this.selectionStart = -1;
-          this.selectionEnd = -1;
         }
         break;
 
       case "resize":
         if (!this.isHandlingFocus)
           return;
 
         if (this.scrollIntoViewTimeout) {
@@ -691,24 +686,16 @@ var FormAssistant = {
         requestId: json.requestId,
         error: "Expected contextId " + this._focusCounter +
                " but was " + json.contextId
       });
       return;
     }
 
     if (!target) {
-      switch (msg.name) {
-      case "Forms:GetText":
-        sendAsyncMessage("Forms:GetText:Result:Error", {
-          requestId: json.requestId,
-          error: "No focused element"
-        });
-        break;
-      }
       return;
     }
 
     this._editing = true;
     switch (msg.name) {
       case "Forms:Input:Value": {
         CompositionManager.endComposition('');
 
@@ -930,34 +917,16 @@ var FormAssistant = {
           sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
             requestId: json.requestId,
             selectioninfo: this.getSelectionInfo()
           });
         }
         break;
       }
 
-      case "Forms:GetText": {
-        let value = isContentEditable(target) ? getContentEditableText(target)
-                                              : target.value;
-
-        if (json.offset && json.length) {
-          value = value.substr(json.offset, json.length);
-        }
-        else if (json.offset) {
-          value = value.substr(json.offset);
-        }
-
-        sendAsyncMessage("Forms:GetText:Result:OK", {
-          requestId: json.requestId,
-          text: value
-        });
-        break;
-      }
-
       case "Forms:GetContext": {
         let obj = getJSON(target, this._focusCounter);
         sendAsyncMessage("Forms:GetContext:Result:OK", obj);
         break;
       }
 
       case "Forms:SetComposition": {
         CompositionManager.setComposition(target, json.text, json.cursor,
@@ -992,16 +961,19 @@ var FormAssistant = {
     this.setFocusedElement(target);
     this.sendInputState(target);
     this.isHandlingFocus = true;
   },
 
   unhandleFocus: function fa_unhandleFocus() {
     this.setFocusedElement(null);
     this.isHandlingFocus = false;
+    this.selectionStart = -1;
+    this.selectionEnd = -1;
+    this.text = "";
     sendAsyncMessage("Forms:Blur", {});
   },
 
   isFocusableElement: function fa_isFocusableElement(element) {
     if (element instanceof HTMLSelectElement ||
         element instanceof HTMLTextAreaElement)
       return true;
 
@@ -1031,33 +1003,28 @@ var FormAssistant = {
 
   getSelectionInfo: function fa_getSelectionInfo() {
     let element = this.focusedElement;
     let range =  getSelectionRange(element);
 
     let text = isContentEditable(element) ? getContentEditableText(element)
                                           : element.value;
 
-    let textAround = getTextAroundCursor(text, range);
-
     let changed = this.selectionStart !== range[0] ||
       this.selectionEnd !== range[1] ||
-      this.textBeforeCursor !== textAround.before ||
-      this.textAfterCursor !== textAround.after;
+      this.text !== text;
 
     this.selectionStart = range[0];
     this.selectionEnd = range[1];
-    this.textBeforeCursor = textAround.before;
-    this.textAfterCursor = textAround.after;
+    this.text = text;
 
     return {
       selectionStart: range[0],
       selectionEnd: range[1],
-      textBeforeCursor: textAround.before,
-      textAfterCursor: textAround.after,
+      text: text,
       changed: changed
     };
   },
 
   _selectionTimeout: null,
 
   // Notify when the selection range changes
   updateSelection: function fa_updateSelection() {
@@ -1152,49 +1119,31 @@ function getJSON(element, focusCounter) 
   let inputMode = element.getAttribute('x-inputmode');
   if (inputMode) {
     inputMode = inputMode.toLowerCase();
   } else {
     inputMode = '';
   }
 
   let range = getSelectionRange(element);
-  let textAround = getTextAroundCursor(value, range);
 
   return {
     "contextId": focusCounter,
 
     "type": type,
     "inputType": inputType,
     "inputMode": inputMode,
 
     "choices": getListForElement(element),
     "value": value,
     "selectionStart": range[0],
     "selectionEnd": range[1],
     "max": max,
     "min": min,
-    "lang": element.lang || "",
-    "textBeforeCursor": textAround.before,
-    "textAfterCursor": textAround.after
-  };
-}
-
-function getTextAroundCursor(value, range) {
-  let textBeforeCursor = range[0] < 100 ?
-    value.substr(0, range[0]) :
-    value.substr(range[0] - 100, 100);
-
-  let textAfterCursor = range[1] + 100 > value.length ?
-    value.substr(range[0], value.length) :
-    value.substr(range[0], range[1] - range[0] + 100);
-
-  return {
-    before: textBeforeCursor,
-    after: textAfterCursor
+    "lang": element.lang || ""
   };
 }
 
 function getListForElement(element) {
   if (!(element instanceof HTMLSelectElement))
     return null;
 
   let optionIndex = 0;
--- a/dom/inputmethod/mochitest/test_basic.html
+++ b/dom/inputmethod/mochitest/test_basic.html
@@ -37,16 +37,17 @@ function runTest() {
       inputmethod_cleanup();
       return;
     }
 
     is(gContext.type, 'input', 'The input context type should match.');
     is(gContext.inputType, 'text', 'The inputType should match.');
     is(gContext.inputMode, 'verbatim', 'The inputMode should match.');
     is(gContext.lang, 'zh', 'The language should match.');
+    is(gContext.text, 'Yuan', 'Should get the text.');
     is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan',
        'Should get the text around the cursor.');
 
     test_setSelectionRange();
   };
 
   // Set current page as an input method.
   SpecialPowers.wrap(im).setActive(true);
@@ -60,50 +61,57 @@ function runTest() {
 function test_setSelectionRange() {
   // Move cursor position to 2.
   gContext.setSelectionRange(2, 0).then(function() {
     is(gContext.selectionStart, 2, 'selectionStart was set successfully.');
     is(gContext.selectionEnd, 2, 'selectionEnd was set successfully.');
     test_sendKey();
   }, function(e) {
     ok(false, 'setSelectionRange failed:' + e.name);
+    console.error(e);
     inputmethod_cleanup();
   });
 }
 
 function test_sendKey() {
   // Add '-' to current cursor posistion and move the cursor position to 3.
   gContext.sendKey(0, '-'.charCodeAt(0), 0).then(function() {
+    is(gContext.text, 'Yu-an',
+       'sendKey should changed the input field correctly.');
     is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yu-an',
        'sendKey should changed the input field correctly.');
     test_deleteSurroundingText();
   }, function(e) {
     ok(false, 'sendKey failed:' + e.name);
     inputmethod_cleanup();
   });
 }
 
 function test_deleteSurroundingText() {
   // Remove one character before current cursor position and move the cursor
   // position back to 2.
   gContext.deleteSurroundingText(-1, 1).then(function() {
     ok(true, 'deleteSurroundingText finished');
+    is(gContext.text, 'Yuan',
+       'deleteSurroundingText should changed the input field correctly.');
     is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan',
        'deleteSurroundingText should changed the input field correctly.');
     test_replaceSurroundingText();
   }, function(e) {
     ok(false, 'deleteSurroundingText failed:' + e.name);
     inputmethod_cleanup();
   });
 }
 
 function test_replaceSurroundingText() {
   // Replace 'Yuan' with 'Xulei'.
   gContext.replaceSurroundingText('Xulei', -2, 4).then(function() {
     ok(true, 'replaceSurroundingText finished');
+    is(gContext.text, 'Xulei',
+       'replaceSurroundingText changed the input field correctly.');
     is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei',
        'replaceSurroundingText changed the input field correctly.');
     test_setComposition();
   }, function(e) {
     ok(false, 'replaceSurroundingText failed: ' + e.name);
     inputmethod_cleanup();
   });
 }
@@ -115,16 +123,18 @@ function test_setComposition() {
   }, function(e) {
     ok(false, 'setComposition failed: ' + e.name);
     inputmethod_cleanup();
   });
 }
 
 function test_endComposition() {
   gContext.endComposition('2013').then(function() {
+    is(gContext.text, 'Xulei2013',
+       'endComposition changed the input field correctly.');
     is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei2013',
        'endComposition changed the input field correctly.');
     test_onSelectionChange();
   }, function (e) {
     ok(false, 'endComposition failed: ' + e.name);
     inputmethod_cleanup();
   });
 }
@@ -176,18 +186,19 @@ function test_onSurroundingTextChange() 
     else {
       // in case we want more tests leave this
       inputmethod_cleanup();
     }
   }
 
   gContext.onsurroundingtextchange = function(evt) {
     ok(true, 'onsurroundingtextchange fired');
-    is(evt.detail.beforeString, 'Xulei2013jj');
-    is(evt.detail.afterString, '');
+    is(evt.detail.text, 'Xulei2013jj');
+    is(evt.detail.textBeforeCursor, 'Xulei2013jj');
+    is(evt.detail.textAfterCursor, '');
     ok(evt.detail.ownAction);
   };
 
   gContext.sendKey(0, 'j'.charCodeAt(0), 0).then(function() {
     cleanup();
   }, function(e) {
     ok(false, 'sendKey failed: ' + e.name);
     cleanup(true);
--- a/dom/webidl/InputMethod.webidl
+++ b/dom/webidl/InputMethod.webidl
@@ -240,17 +240,17 @@ interface MozInputContextFocusEventDetai
   readonly attribute MozInputMethodInputContextInputTypes inputType;
 
   /**
    * The following is only needed for rendering and handling "option" input types,
    * in System app.
    */
 
   /**
-   * Current value of the input/select element.
+   * Current value of the input.
    */
   readonly attribute DOMString? value;
   /**
    * An object representing all the <optgroup> and <option> elements
    * in the <select> element.
    */
   [Pure, Cached, Frozen]
   readonly attribute MozInputContextChoicesInfo? choices;
@@ -265,17 +265,17 @@ interface MozInputContextFocusEventDetai
  * Information about the options within the <select> element.
  */
 dictionary MozInputContextChoicesInfo {
   boolean multiple;
   sequence<MozInputMethodChoiceDict> choices;
 };
 
 /**
- * Content the header (<optgroup>) or an option (<option>).
+ * Content of the option header (<optgroup>) or an option (<option>).
  */
 dictionary MozInputMethodChoiceDict {
   boolean group;
   DOMString text;
   boolean disabled;
   boolean? inGroup;
   boolean? selected;
   long? optionIndex;
@@ -353,17 +353,25 @@ interface MozInputContext: EventTarget {
 
   /**
    * The start and stop position of the current selection.
    */
   readonly attribute long selectionStart;
   readonly attribute long selectionEnd;
 
   /**
+   * The text in the current input.
+   */
+  readonly attribute DOMString? text;
+
+  /**
    * The text before and after the begining of the selected text.
+   *
+   * You should use the text property instead because these properties are
+   * truncated at 100 characters.
    */
   readonly attribute DOMString? textBeforeCursor;
   readonly attribute DOMString? textAfterCursor;
 
   /**
    * Set the selection range of the the editable text.
    * Note: This method cannot be used to move the cursor during composition. Calling this
    * method will cancel composition.
@@ -375,19 +383,17 @@ interface MozInputContext: EventTarget {
    *
    * @return boolean
    */
   Promise<boolean> setSelectionRange(long start, long length);
 
   /* User moves the cursor, or changes the selection with other means. If the text around
    * cursor has changed, but the cursor has not been moved, the IME won't get notification.
    *
-   * A dict is provided in the detail property of the event containing the new values, and
-   * an "ownAction" property to denote the event is the result of our own mutation to
-   * the input field.
+   * evt.detail is defined by MozInputContextSelectionChangeEventDetail.
    */
   attribute EventHandler onselectionchange;
 
   /**
    * Commit text to current input field and replace text around
    * cursor position. It will clear the current composition.
    *
    * @param text The string to be replaced with.
@@ -406,19 +412,17 @@ interface MozInputContext: EventTarget {
    */
   Promise<boolean> deleteSurroundingText(long offset, long length);
 
   /**
    * Notifies when the text around the cursor is changed, due to either text
    * editing or cursor movement. If the cursor has been moved, but the text around has not
    * changed, the IME won't get notification.
    *
-   * A dict is provided in the detail property of the event containing the new values, and
-   * an "ownAction" property to denote the event is the result of our own mutation to
-   * the input field.
+   * evt.detail is defined by MozInputContextSurroundingTextChangeEventDetail.
    */
   attribute EventHandler onsurroundingtextchange;
 
   /**
    * Send a string/character with its key events. There are two ways of invocating
    * the method for backward compability purpose.
    *
    * (1) The recommended way, allow specifying DOM level 3 properties like |code|.
@@ -518,16 +522,72 @@ interface MozInputContext: EventTarget {
    * is interrupted by |sendKey|, |setSelectionRange|,
    * |replaceSurroundingText|, |deleteSurroundingText|, user moving the
    * cursor, changing the focus, etc.
    */
   Promise<boolean> endComposition(optional DOMString text,
                                   optional MozInputMethodKeyboardEventDict dict);
 };
 
+/**
+ * Detail of the selectionchange event.
+ */
+[JSImplementation="@mozilla.org/b2g-imm-selectionchange;1",
+ Pref="dom.mozInputMethod.enabled",
+ CheckAnyPermissions="input"]
+interface MozInputContextSelectionChangeEventDetail {
+  /**
+   * Indicate whether or not the change is due to our own action from, 
+   * for example, sendKey() call.
+   *
+   * Note: this property is untrustworthy because it would still be true even
+   * if script in the page changed the text synchronously upon responding to
+   * events trigger by the call.
+   */
+  readonly attribute boolean ownAction;
+
+  /**
+   * The start and stop position of the current selection.
+   */
+  readonly attribute long selectionStart;
+  readonly attribute long selectionEnd;
+};
+
+/**
+ * Detail of the surroundingtextchange event.
+ */
+[JSImplementation="@mozilla.org/b2g-imm-surroundingtextchange;1",
+ Pref="dom.mozInputMethod.enabled",
+ CheckAnyPermissions="input"]
+interface MozInputContextSurroundingTextChangeEventDetail {
+  /**
+   * Indicate whether or not the change is due to our own action from, 
+   * for example, sendKey() call.
+   *
+   * Note: this property is untrustworthy because it would still be true even
+   * if script in the page changed the text synchronously upon responding to
+   * events trigger by the call.
+   */
+  readonly attribute boolean ownAction;
+
+  /**
+   * The text in the current input.
+   */
+  readonly attribute DOMString? text;
+
+  /**
+   * The text before and after the begining of the selected text.
+   *
+   * You should use the text property instead because these properties are
+   * truncated at 100 characters.
+   */
+  readonly attribute DOMString? textBeforeCursor;
+  readonly attribute DOMString? textAfterCursor;
+};
+
 enum CompositionClauseSelectionType {
   "raw-input",
   "selected-raw-text",
   "converted-text",
   "selected-converted-text"
 };
 
 dictionary CompositionClauseParameters {