Bug 818893 - Get caret position of the contenteditable r=djf
authorYuan Xulei <xyuan@mozilla.com>
Tue, 01 Jan 2013 19:26:49 +0800
changeset 123132 4cf6c8896c93f37fb8da0bcdf5b0da7fedd1949b
parent 123131 b082c2abd269d560772cdb59af0eddfecd5e6539
child 123133 25a2e989162c95c4f1296e2977dedcbf9e49c8bb
push id24372
push useremorley@mozilla.com
push dateWed, 27 Feb 2013 13:22:59 +0000
treeherdermozilla-central@0a91da5f5eab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdjf
bugs818893
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 818893 - Get caret position of the contenteditable r=djf
b2g/chrome/content/forms.js
--- a/b2g/chrome/content/forms.js
+++ b/b2g/chrome/content/forms.js
@@ -194,16 +194,17 @@ let FormAssistant = {
     'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image'
   ]),
 
   isKeyboardOpened: false,
   selectionStart: 0,
   selectionEnd: 0,
   scrollIntoViewTimeout: null,
   _focusedElement: null,
+  _documentEncoder: null,
 
   get focusedElement() {
     if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
       this._focusedElement = null;
 
     return this._focusedElement;
   },
 
@@ -218,28 +219,37 @@ let FormAssistant = {
     if (this.focusedElement) {
       this.focusedElement.removeEventListener('mousedown', this);
       this.focusedElement.removeEventListener('mouseup', this);
       if (!element) {
         this.focusedElement.blur();
       }
     }
 
+    this._documentEncoder = null;
+
     if (element) {
       element.addEventListener('mousedown', this);
       element.addEventListener('mouseup', this);
+      if (isContentEditable(element)) {
+        this._documentEncoder = getDocumentEncoder(element);
+      }
     }
 
     this.focusedElement = element;
   },
 
+  get documentEncoder() {
+    return this._documentEncoder;
+  },
+
   handleEvent: function fa_handleEvent(evt) {
-    let focusedElement = this.focusedElement;
     let target = evt.target;
 
+    let range = null;
     switch (evt.type) {
       case "focus":
         if (!target) {
           break;
         }
 
         if (target instanceof HTMLDocument || target == content) {
           break;
@@ -259,27 +269,29 @@ let FormAssistant = {
       case "pagehide":
         if (this.focusedElement)
           this.hideKeyboard();
         break;
 
       case 'mousedown':
         // We only listen for this event on the currently focused element.
         // When the mouse goes down, note the cursor/selection position
-        this.selectionStart = this.focusedElement.selectionStart;
-        this.selectionEnd = this.focusedElement.selectionEnd;
+        range = getSelectionRange(this.focusedElement);
+        this.selectionStart = range[0];
+        this.selectionEnd = range[1];
         break;
 
       case 'mouseup':
         // We only listen for this event on the currently focused element.
         // When the mouse goes up, see if the cursor has moved (or the
         // selection changed) since the mouse went down. If it has, we
         // need to tell the keyboard about it
-        if (this.focusedElement.selectionStart !== this.selectionStart ||
-            this.focusedElement.selectionEnd !== this.selectionEnd) {
+        range = getSelectionRange(this.focusedElement);
+        if (range[0] !== this.selectionStart ||
+            range[1] !== this.selectionEnd) {
           this.sendKeyboardState(this.focusedElement);
         }
         break;
 
       case "resize":
         if (!this.isKeyboardOpened)
           return;
 
@@ -354,21 +366,21 @@ let FormAssistant = {
 
   showKeyboard: function fa_showKeyboard(target) {
     if (this.isKeyboardOpened)
       return;
 
     if (target instanceof HTMLOptionElement)
       target = target.parentNode;
 
+    this.setFocusedElement(target);
+
     let kbOpened = this.sendKeyboardState(target);
     if (this.isTextInputElement(target))
       this.isKeyboardOpened = kbOpened;
-
-    this.setFocusedElement(target);
   },
 
   hideKeyboard: function fa_hideKeyboard() {
     sendAsyncMessage("Forms:Input", { "type": "blur" });
     this.isKeyboardOpened = false;
     this.setFocusedElement(null);
   },
 
@@ -425,17 +437,16 @@ let FormAssistant = {
 
     sendAsyncMessage("Forms:Input", getJSON(element));
     return true;
   }
 };
 
 FormAssistant.init();
 
-
 function isContentEditable(element) {
   if (element.isContentEditable || element.designMode == "on")
     return true;
 
   // If a body element is editable and the body is the child of an
   // iframe we can assume this is an advanced HTML editor
   if (element instanceof HTMLIFrameElement &&
       element.contentDocument &&
@@ -443,22 +454,22 @@ function isContentEditable(element) {
        element.contentDocument.designMode == "on"))
     return true;
 
   return element.ownerDocument && element.ownerDocument.designMode == "on";
 }
 
 function getJSON(element) {
   let type = element.type || "";
-  let value = element.value || ""
+  let value = element.value || "";
 
-  // Treat contenteditble element as a special text field
+  // Treat contenteditble element as a special text area field
   if (isContentEditable(element)) {
-    type = "text";
-    value = element.textContent;
+    type = "textarea";
+    value = getContentEditableText(element);
   }
 
   // Until the input type=date/datetime/range have been implemented
   // let's return their real type even if the platform returns 'text'
   let attributeType = element.getAttribute("type") || "";
 
   if (attributeType) {
     var typeLowerCase = attributeType.toLowerCase(); 
@@ -479,23 +490,25 @@ function getJSON(element) {
   // solution will be find.
   let inputmode = element.getAttribute('x-inputmode');
   if (inputmode) {
     inputmode = inputmode.toLowerCase();
   } else {
     inputmode = '';
   }
 
+  let range = getSelectionRange(element);
+
   return {
     "type": type.toLowerCase(),
     "choices": getListForElement(element),
     "value": value,
     "inputmode": inputmode,
-    "selectionStart": element.selectionStart,
-    "selectionEnd": element.selectionEnd
+    "selectionStart": range[0],
+    "selectionEnd": range[1]
   };
 }
 
 function getListForElement(element) {
   if (!(element instanceof HTMLSelectElement))
     return null;
 
   let optionIndex = 0;
@@ -541,8 +554,56 @@ function getListForElement(element) {
         "optionIndex": optionIndex++
       });
     }
   }
 
   return result;
 };
 
+// Create a plain text document encode from the focused element.
+function getDocumentEncoder(element) {
+  let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
+                .createInstance(Ci.nsIDocumentEncoder);
+  let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent |
+              Ci.nsIDocumentEncoder.OutputRaw |
+              Ci.nsIDocumentEncoder.OutputLFLineBreak |
+              Ci.nsIDocumentEncoder.OutputDropInvisibleBreak;
+  encoder.init(element.ownerDocument, "text/plain", flags);
+  return encoder;
+}
+
+// Get the visible content text of a content editable element
+function getContentEditableText(element) {
+  let doc = element.ownerDocument;
+  let range = doc.createRange();
+  range.selectNodeContents(element);
+  let encoder = FormAssistant.documentEncoder;
+  encoder.setRange(range);
+  return encoder.encodeToString();
+}
+
+function getSelectionRange(element) {
+  let start = 0;
+  let end = 0;
+  if (element instanceof HTMLInputElement ||
+      element instanceof HTMLTextAreaElement) {
+    // Get the selection range of <input> and <textarea> elements
+    start = element.selectionStart;
+    end = element.selectionEnd;
+  } else {
+    // Get the selection range of contenteditable elements
+    let win = element.ownerDocument.defaultView;
+    let sel = win.getSelection();
+
+    let range = win.document.createRange();
+    range.setStart(element, 0);
+    range.setEnd(sel.anchorNode, sel.anchorOffset);
+    let encoder = FormAssistant.documentEncoder;
+
+    encoder.setRange(range);
+    start = encoder.encodeToString().length;
+
+    encoder.setRange(sel.getRangeAt(0));
+    end = start + encoder.encodeToString().length;
+  }
+  return [start, end];
+}