Bug 628616 - Make sure suggestions from <datalist> are shown in Firefox Mobile UI [r=mfinkle]
authorVivien Nicolas <21@vingtetun.org>
Thu, 31 Mar 2011 02:59:55 +0200
changeset 67557 6ec43c0829382dee5534be71cba2b37fc51a159b
parent 67556 eae1f1fb4fdcc37bba43a47b5e98017473dfc2a2
child 67558 321da6243809c3a73e17c529f933a1fba50e544a
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs628616
Bug 628616 - Make sure suggestions from <datalist> are shown in Firefox Mobile UI [r=mfinkle]
mobile/chrome/content/common-ui.js
mobile/chrome/content/content.css
mobile/chrome/content/forms.js
mobile/chrome/tests/Makefile.in
mobile/chrome/tests/browser_autocomplete.html
mobile/chrome/tests/browser_autocomplete.js
mobile/chrome/tests/remote_autocomplete.js
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -656,18 +656,19 @@ var FormHelperUI = {
     let lastElement = this._currentElement || null;
     this._currentElement = {
       id: aElement.id,
       name: aElement.name,
       title: aElement.title,
       value: aElement.value,
       maxLength: aElement.maxLength,
       type: aElement.type,
+      choices: aElement.choices,
       isAutocomplete: aElement.isAutocomplete,
-      list: aElement.choices
+      list: aElement.list,
     }
 
     this._updateContainerForSelect(lastElement, this._currentElement);
     this._zoom(Rect.fromRect(aElement.rect), Rect.fromRect(aElement.caretRect));
     this._updateSuggestionsFor(this._currentElement);
 
     // Prevent the view to scroll automatically while typing
     this._currentBrowser.scrollSync = false;
@@ -830,17 +831,17 @@ var FormHelperUI = {
 
   goToNext: function formHelperGoToNext() {
     this._currentBrowser.messageManager.sendAsyncMessage("FormAssist:Next", { });
   },
 
   doAutoComplete: function formHelperDoAutoComplete(aElement) {
     // Suggestions are only in <label>s. Ignore the rest.
     if (aElement instanceof Ci.nsIDOMXULLabelElement)
-      this._currentBrowser.messageManager.sendAsyncMessage("FormAssist:AutoComplete", { value: aElement.value });
+      this._currentBrowser.messageManager.sendAsyncMessage("FormAssist:AutoComplete", { value: aElement.getAttribute("data") });
   },
 
   get _open() {
     return (this._container.getAttribute("type") == this.type);
   },
 
   set _open(aVal) {
     if (aVal == this._open)
@@ -897,19 +898,20 @@ var FormHelperUI = {
     }
 
     let container = suggestionsContainer.firstChild;
     while (container.hasChildNodes())
       container.removeChild(container.lastChild);
 
     let fragment = document.createDocumentFragment();
     for (let i = 0; i < suggestions.length; i++) {
-      let value = suggestions[i];
+      let suggestion = suggestions[i];
       let button = document.createElement("label");
-      button.setAttribute("value", value);
+      button.setAttribute("value", suggestion.label);
+      button.setAttribute("data", suggestion.value);
       button.className = "form-helper-suggestions-label";
       fragment.appendChild(button);
     }
     container.appendChild(fragment);
 
     this._hasSuggestions = true;
     this._ensureSuggestionsVisible();
   },
@@ -926,20 +928,26 @@ var FormHelperUI = {
     if (results.matchCount > 0) {
       for (let i = 0; i < results.matchCount; i++) {
         let value = results.getValueAt(i);
 
         // Do not show the value if it is the current one in the input field
         if (value == aElement.value)
           continue;
 
-        suggestions.push(value);
+        suggestions.push({ "label": value, "value": value});
       }
     }
 
+    // Add the datalist elements provided by the website, note that the
+    // displayed value can differ from the real value of the element.
+    let options = aElement.list;
+    for (let i = 0; i < options.length; i++)
+      suggestions.push(options[i]);
+
     return suggestions;
   },
 
   _resetSuggestions: function _formHelperResetAutocomplete() {
     this._suggestionsContainer.hidden = true;
     this._hasSuggestions = false;
   },
 
@@ -1037,21 +1045,21 @@ var FormHelperUI = {
   _updateContainer: function _formHelperUpdateContainer(aLastElement, aCurrentElement) {
     this._updateContainerForSelect(aLastElement, aCurrentElement);
 
     this._container.contentHasChanged();
   },
 
   /** Helper for _updateContainer that handles the case where the new element is a select. */
   _updateContainerForSelect: function _formHelperUpdateContainerForSelect(aLastElement, aCurrentElement) {
-    let lastHasChoices = aLastElement && (aLastElement.list != null);
-    let currentHasChoices = aCurrentElement && (aCurrentElement.list != null);
+    let lastHasChoices = aLastElement && (aLastElement.choices != null);
+    let currentHasChoices = aCurrentElement && (aCurrentElement.choices != null);
 
     if (currentHasChoices)
-      SelectHelperUI.show(aCurrentElement.list, aCurrentElement.title);
+      SelectHelperUI.show(aCurrentElement.choices, aCurrentElement.title);
     else if (lastHasChoices)
       SelectHelperUI.hide();
   },
 
   /** Zoom and move viewport so that element is legible and touchable. */
   _zoom: function _formHelperZoom(aElementRect, aCaretRect) {
     let browser = getBrowser();
     let zoomRect = Rect.fromRect(browser.getBoundingClientRect());
--- a/mobile/chrome/content/content.css
+++ b/mobile/chrome/content/content.css
@@ -334,24 +334,16 @@ button:active,
 input:active,
 option:active,
 select:active,
 label:active,
 textarea:active {
   background-color: rgba(141, 184, 216, 0.5) !important;
 }
 
-/*  
- * Until datalist are fully supported on mobile (bug 628616) set the display
- * to inline in order to allow fallback to works
- */
-datalist {
-  display: inline;
-}
-
 /* 
  * Generate an additional space after the anonymous div to make it easier to
  * to position the caret at the end of the text
  */
 input > .anonymous-div:after {
   content: "";
   margin: 16px;
 }
--- a/mobile/chrome/content/forms.js
+++ b/mobile/chrome/content/forms.js
@@ -503,16 +503,48 @@ FormAssistant.prototype = {
       let allowedValues = ["off", "false", "disabled"];
       if (allowedValues.indexOf(autocomplete) == -1)
         return true;
     }
 
     return false;
   },
 
+  /*
+   * This function is similar to getListSuggestions from
+   * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
+   * used by the autocomplete.xml binding which is not in used in fennec
+   */
+  _getListSuggestions: function formHelperGetListSuggestions(aElement) {
+    if (!(aElement instanceof HTMLInputElement) || !aElement.list)
+      return [];
+
+    let suggestions = [];
+    let filter = !aElement.hasAttribute("mozNoFilter");
+    let lowerFieldValue = aElement.value.toLowerCase();
+
+    let options = aElement.list.options;
+    let length = options.length;
+    for (let i = 0; i < length; i++) {
+      let item = options.item(i);
+
+      let label = item.value;
+      if (item.label)
+        label = item.label;
+      else if (item.text)
+        label = item.text;
+
+      if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1)
+        continue;
+       suggestions.push({ label: label, value: item.value });
+    }
+
+    return suggestions;
+  },
+
   _isValidElement: function formHelperIsValidElement(aElement) {
     if (!aElement.getAttribute)
       return false;
 
     let formExceptions = { button: true, checkbox: true, file: true, image: true, radio: true, reset: true, submit: true };
     if (aElement instanceof HTMLInputElement && formExceptions[aElement.type])
       return false;
 
@@ -659,28 +691,30 @@ FormAssistant.prototype = {
       if (elements[i] == aElement)
         return i;
     }
     return -1;
   },
 
   _getJSON: function() {
     let element = this.currentElement;
-    let list = getListForElement(element);
+    let choices = getListForElement(element);
+
     let labels = this._getLabels();
     return {
       current: {
         id: element.id,
         name: element.name,
         title: labels.length ? labels[0].title : "",
         value: element.value,
         maxLength: element.maxLength,
         type: (element.getAttribute("type") || "").toLowerCase(),
-        choices: list,
+        choices: choices,
         isAutocomplete: this._isAutocomplete(this.currentElement),
+        list: this._getListSuggestions(this.currentElement),
         rect: this._getRect(),
         caretRect: this._getCaretRect()
       },
       hasPrevious: !!this._elements[this._currentIndex - 1],
       hasNext: !!this._elements[this._currentIndex + 1]
     };
   },
 
--- a/mobile/chrome/tests/Makefile.in
+++ b/mobile/chrome/tests/Makefile.in
@@ -43,21 +43,25 @@ relativesrcdir  = mobile/chrome
 TESTXPI  = $(CURDIR)/$(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)/addons
 ADDONSRC = $(srcdir)/addons
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_FILES = \
   head.js \
+  remote_autocomplete.js \
   remote_head.js \
   remote_forms.js \
   remote_formsZoom.js \
   remote_vkb.js \
   browser_addons.js \
+  browser_autocomplete.html \
+  browser_autocomplete.js \
+  browser_autocompletesearch.js\
   browser_awesomescreen.js \
   browser_blank_01.html \
   browser_blank_02.html \
   browser_bookmarks.js \
   browser_bookmarks_star.js \
   browser_bookmarks_tags.js \
   browser_click_content.html \
   browser_click_content.js \
@@ -84,17 +88,16 @@ include $(topsrcdir)/config/rules.mk
   browser_vkb.js \
   browser_viewport.js \
   browser_viewport.sjs \
   browser_scrollbar.sjs \
   browser_title.sjs \
   browser_thumbnails.js \
   browser_install.xml \
   browser_upgrade.rdf\
-  browser_autocompletesearch.js\
   mock_autocomplete.json\
   $(NULL)
 
 ifndef ANDROID
 ifndef MOZ_PLATFORM_MAEMO
 _BROWSER_FILES +=  browser_sidebars.js
 endif
 endif
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/tests/browser_autocomplete.html
@@ -0,0 +1,26 @@
+<html>
+  <body>
+    <datalist id="datalist-1">
+      <option>foo</option>
+      <option disabled="true">bar</option>
+      <option value="bar">Somewhat bar</option>
+      <option label="foobar" value="_"></option>
+    </datalist>
+    <input id="input-datalist-1" list="datalist-1"></input>
+
+    <br /><br />
+
+    <datalist id="datalist-2">
+      <option>foo</option>
+      <option>bar</option>
+      <option>foobar</option>
+      <option>foobar foo</option>
+      <option>foobar foo titi</option>
+      <option>foobar foo titi toto</option>
+      <option>foobar foo titi toto tutu</option>
+    </datalist>
+    <input id="input-datalist-2" list="datalist-2"></input>
+
+  </body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/tests/browser_autocomplete.js
@@ -0,0 +1,140 @@
+let testURL = chromeRoot + "browser_autocomplete.html";
+messageManager.loadFrameScript(chromeRoot + "remote_autocomplete.js", true);
+
+let newTab = null;
+
+// A queue to order the tests and a handle for each test
+var gTests = [];
+var gCurrentTest = null;
+
+function test() {
+  // This test is async
+  waitForExplicitFinish();
+
+  // Need to wait until the page is loaded
+  messageManager.addMessageListener("pageshow", function(aMessage) {
+    if (newTab && newTab.browser.currentURI.spec != "about:blank") {
+      messageManager.removeMessageListener(aMessage.name, arguments.callee);
+      BrowserUI.closeAutoComplete(true);
+      setTimeout(runNextTest, 0);
+    }
+  });
+
+  newTab = Browser.addTab(testURL, true);
+}
+
+//------------------------------------------------------------------------------
+// Iterating tests by shifting test out one by one as runNextTest is called.
+function runNextTest() {
+  // Run the next test until all tests completed
+  if (gTests.length > 0) {
+    gCurrentTest = gTests.shift();
+    info(gCurrentTest.desc);
+    gCurrentTest.run();
+  }
+  else {
+    // Cleanup. All tests are completed at this point
+    try {
+      // Add any cleanup code here
+    }
+    finally {
+      // We must finialize the tests
+      finish();
+    }
+  }
+}
+
+function waitForAutocomplete(aCallback) {
+  messageManager.addMessageListener("FormAssist:AutoComplete", function(aMessage) {
+    messageManager.removeMessageListener(aMessage.name, arguments.callee);
+    setTimeout(function() {
+      aCallback(aMessage.json.current.list);
+    }, 0);
+  });
+};
+
+let data = [
+  { label: "foo", value: "foo" },
+  { label: "Somewhat bar", value: "bar" },
+  { label: "foobar", value: "_" }
+];
+
+//------------------------------------------------------------------------------
+// Case: Click on a datalist element and show suggestions
+gTests.push({
+  desc: "Click on a datalist element and show suggestions",
+
+  run: function() {
+    waitForAutocomplete(gCurrentTest.checkData);
+    AsyncTests.waitFor("TestRemoteAutocomplete:Click",
+                        { id: "input-datalist-1" }, function(json) {});
+  },
+
+  // Check that the data returned by the autocomplete handler on the content
+  // side is correct
+  checkData: function(aOptions) {
+    for (let i = 0; i < aOptions.length; i++) {
+      let option = aOptions[i];
+      let valid = data[i];
+
+      is(option.label, valid.label, "Label should be equal (" + option.label + ", " + valid.label +")");
+      is(option.value, valid.value, "Value should be equal (" + option.value + ", " + valid.value +")");
+    }
+
+    // Wait until suggestions box has been popupated
+    waitFor(gCurrentTest.checkUI, function() {
+      let suggestionsBox = document.getElementById("form-helper-suggestions");
+      return suggestionsBox.childNodes.length;
+    });
+  },
+
+  // Check that the UI reflect the specificity of the data
+  checkUI: function() {
+    let suggestionsBox = document.getElementById("form-helper-suggestions");
+    let suggestions = suggestionsBox.childNodes;
+
+    for (let i = 0; i < suggestions.length; i++) {
+      let suggestion = suggestions[i];
+      let valid = data[i];
+      let label = suggestion.getAttribute("value");
+      let value = suggestion.getAttribute("data");
+
+      is(label, valid.label, "Label should be equal (" + label + ", " + valid.label +")");
+      is(value, valid.value, "Value should be equal (" + value + ", " + valid.value +")");
+    }
+
+    gCurrentTest.checkUIClick(0);
+  },
+
+  // Ensure that clicking on a given datalist element set the right value in
+  // the input box
+  checkUIClick: function(aIndex) {
+    let suggestionsBox = document.getElementById("form-helper-suggestions");
+
+    let suggestion = suggestionsBox.childNodes[aIndex];
+    if (!suggestion) {
+      gCurrentTest.finish();
+      return;
+    }
+
+    // Use the form helper autocompletion helper
+    FormHelperUI.doAutoComplete(suggestion);
+
+    AsyncTests.waitFor("TestRemoteAutocomplete:Check", { id: "input-datalist-1" }, function(json) {
+      is(json.result, suggestion.getAttribute("data"), "The target input value should be set to " + data);
+      gCurrentTest.checkUIClick(aIndex + 1);
+    });
+  },
+
+  finish: function() {
+    // Close the form assistant
+    FormHelperUI.hide();
+
+    // Close our tab when finished
+    Browser.closeTab(newTab);
+
+    // We must finalize the tests
+    finish();
+  }
+});
+
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/tests/remote_autocomplete.js
@@ -0,0 +1,18 @@
+dump("====================== Content Script Loaded =======================\n");
+
+let assistant = Content._formAssistant;
+
+AsyncTests.add("TestRemoteAutocomplete:Click", function(aMessage, aJson) {
+  let element = content.document.getElementById(aJson.id);
+  assistant.open(element);
+  assistant._executeDelayed(function(assistant) {
+    sendAsyncMessage("FormAssist:AutoComplete", assistant._getJSON());
+  });
+  return true;
+});
+
+AsyncTests.add("TestRemoteAutocomplete:Check", function(aMessage, aJson) {
+  let element = content.document.getElementById(aJson.id);
+  return element.value;
+});
+