Bug 844744, part 2 - Make <input type=number> use ICU when available to support localized number input. r=smaug
authorJonathan Watt <jwatt@jwatt.org>
Thu, 23 Jan 2014 15:43:12 +0000
changeset 166520 2682af062a4bb866c9de8da6527e15365b86ee4d
parent 166519 340786b6f4dd21e3036c117e33fea3fd97925476
child 166521 428d06e835898c3bf1848f56fb7193affaee8058
push id26128
push userphilringnalda@gmail.com
push dateSun, 02 Feb 2014 17:23:15 +0000
treeherdermozilla-central@5f88d54c28e0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs844744
milestone29.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 844744, part 2 - Make <input type=number> use ICU when available to support localized number input. r=smaug
content/html/content/src/HTMLInputElement.cpp
content/html/content/src/HTMLInputElement.h
content/html/content/test/forms/mochitest.ini
content/html/content/test/forms/test_input_number_l10n.html
layout/forms/moz.build
layout/forms/nsNumberControlFrame.cpp
layout/forms/nsNumberControlFrame.h
--- a/content/html/content/src/HTMLInputElement.cpp
+++ b/content/html/content/src/HTMLInputElement.cpp
@@ -1448,16 +1448,27 @@ HTMLInputElement::AfterSetAttr(int32_t a
         GetValue(value);
         SetValueInternal(value, false, false);
         MOZ_ASSERT(!GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
                    "HTML5 spec does not allow this");
       }
     } else if (aName == nsGkAtoms::dir &&
                aValue && aValue->Equals(nsGkAtoms::_auto, eIgnoreCase)) {
       SetDirectionIfAuto(true, aNotify);
+    } else if (aName == nsGkAtoms::lang) {
+      if (mType == NS_FORM_INPUT_NUMBER) {
+        // Update the value that is displayed to the user to the new locale:
+        nsAutoString value;
+        GetValueInternal(value);
+        nsNumberControlFrame* numberControlFrame =
+          do_QueryFrame(GetPrimaryFrame());
+        if (numberControlFrame) {
+          numberControlFrame->SetValueOfAnonTextControl(value);
+        }
+      }
     }
 
     UpdateState(aNotify);
   }
 
   return nsGenericHTMLFormElementWithState::AfterSetAttr(aNameSpaceID, aName,
                                                          aValue, aNotify);
 }
@@ -1660,17 +1671,18 @@ HTMLInputElement::IsValueEmpty() const
 
 void
 HTMLInputElement::ClearFiles(bool aSetValueChanged)
 {
   nsTArray<nsCOMPtr<nsIDOMFile> > files;
   SetFiles(files, aSetValueChanged);
 }
 
-static Decimal StringToDecimal(nsAString& aValue)
+/* static */ Decimal
+HTMLInputElement::StringToDecimal(const nsAString& aValue)
 {
   if (!IsASCII(aValue)) {
     return Decimal::nan();
   }
   NS_LossyConvertUTF16toASCII asciiString(aValue);
   std::string stdString = asciiString.get();
   return Decimal::fromString(stdString);
 }
@@ -2768,17 +2780,17 @@ HTMLInputElement::SetValueInternal(const
           SetValueChanged(true);
         }
         OnValueChanged(!mParserCreating);
 
         if (mType == NS_FORM_INPUT_NUMBER) {
           nsNumberControlFrame* numberControlFrame =
             do_QueryFrame(GetPrimaryFrame());
           if (numberControlFrame) {
-            numberControlFrame->UpdateForValueChange(value);
+            numberControlFrame->SetValueOfAnonTextControl(value);
           }
         }
       }
 
       // Call parent's SetAttr for color input so its control frame is notified
       // and updated
       if (mType == NS_FORM_INPUT_COLOR) {
         return nsGenericHTMLFormElement::SetAttr(kNameSpaceID_None,
@@ -3450,19 +3462,19 @@ HTMLInputElement::PreHandleEvent(nsEvent
     nsNumberControlFrame* numberControlFrame =
       do_QueryFrame(GetPrimaryFrame());
     if (numberControlFrame) {
       textControl = numberControlFrame->GetAnonTextControl();
     }
     if (textControl && aVisitor.mEvent->originalTarget == textControl) {
       if (aVisitor.mEvent->message == NS_FORM_INPUT) {
         // Propogate the anon text control's new value to our HTMLInputElement:
+        nsAutoString value;
+        numberControlFrame->GetValueOfAnonTextControl(value);
         numberControlFrame->HandlingInputEvent(true);
-        nsAutoString value;
-        textControl->GetValue(value);
         nsWeakFrame weakNumberControlFrame(numberControlFrame);
         SetValueInternal(value, false, true);
         if (weakNumberControlFrame.IsAlive()) {
           numberControlFrame->HandlingInputEvent(false);
         }
       }
       else if (aVisitor.mEvent->message == NS_FORM_CHANGE) {
         // We cancel the DOM 'change' event that is fired for any change to our
--- a/content/html/content/src/HTMLInputElement.h
+++ b/content/html/content/src/HTMLInputElement.h
@@ -710,16 +710,27 @@ public:
   bool MozIsTextField(bool aExcludePassword);
 
   nsIEditor* GetEditor();
 
   // XPCOM SetUserInput() is OK
 
   // XPCOM GetPhonetic() is OK
 
+  /**
+   * If aValue contains a valid floating-point number in the format specified
+   * by the HTML 5 spec:
+   *
+   *   http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#floating-point-numbers
+   *
+   * then this function will return the number parsed as a Decimal, otherwise
+   * it will return a Decimal for which Decimal::isFinite() will return false.
+   */
+  static Decimal StringToDecimal(const nsAString& aValue);
+
 protected:
   virtual JSObject* WrapNode(JSContext* aCx,
                              JS::Handle<JSObject*> aScope) MOZ_OVERRIDE;
 
   // Pull IsSingleLineTextControl into our scope, otherwise it'd be hidden
   // by the nsITextControlElement version.
   using nsGenericHTMLFormElementWithState::IsSingleLineTextControl;
 
--- a/content/html/content/test/forms/mochitest.ini
+++ b/content/html/content/test/forms/mochitest.ini
@@ -18,16 +18,19 @@ support-files =
 [test_input_color_input_change_events.html]
 [test_input_color_picker_initial.html]
 [test_input_color_picker_popup.html]
 [test_input_color_picker_update.html]
 [test_input_email.html]
 [test_input_event.html]
 [test_input_file_picker.html]
 [test_input_list_attribute.html]
+[test_input_number_l10n.html]
+# We don't build ICU for Firefox for Android or Firefox OS:
+skip-if = os == "android" || appname == "b2g"
 [test_input_number_key_events.html]
 [test_input_number_mouse_events.html]
 # Not run on Firefox OS and Firefox for Android where the spin buttons are hidden:
 skip-if = os == "android" || appname == "b2g"
 [test_input_number_rounding.html]
 skip-if = os == "android"
 [test_input_range_attr_order.html]
 [test_input_range_key_events.html]
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/forms/test_input_number_l10n.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=844744
+-->
+<head>
+  <title>Test localization of number control input</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844744">Mozilla Bug 844744</a>
+<p id="display"></p>
+<div id="content">
+  <input id="input" type="number" step="any">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 844744
+ * This test checks that localized input that is typed into <input type=number>
+ * is correctly handled.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+  startTests();
+  SimpleTest.finish();
+});
+
+var tests = [
+  { desc: "British English",
+    langTag: "en-GB", inputWithGrouping: "123,456.78",
+    inputWithoutGrouping: "123456.78", value: 123456.78
+  },
+  { desc: "Farsi",
+    langTag: "fa", inputWithGrouping: "۱۲۳٬۴۵۶٫۷۸",
+    inputWithoutGrouping: "۱۲۳۴۵۶٫۷۸", value: 123456.78
+  },
+  { desc: "French",
+    langTag: "fr-FR", inputWithGrouping: "123 456,78",
+    inputWithoutGrouping: "123456,78", value: 123456.78
+  },
+  { desc: "German",
+    langTag: "de", inputWithGrouping: "123.456,78",
+    inputWithoutGrouping: "123456,78", value: 123456.78
+  },
+  { desc: "Hebrew",
+    langTag: "he", inputWithGrouping: "123,456.78",
+    inputWithoutGrouping: "123456.78", value: 123456.78
+  },
+];
+
+var elem;
+
+function runTest(test) {
+  elem.lang = test.langTag;
+  elem.value = 0;
+  elem.focus();
+  elem.select();
+  sendString(test.inputWithGrouping);
+  is(elem.value, test.value, "Test " + test.desc + " ('" + test.langTag +
+                             "') localization with grouping separator");
+  elem.value = 0;
+  elem.select();
+  sendString(test.inputWithoutGrouping);
+  is(elem.value, test.value, "Test " + test.desc + " ('" + test.langTag +
+                             "') localization without grouping separator");
+}
+
+function startTests() {
+  elem = document.getElementById("input");
+  for (var test of tests) {
+    runTest(test, elem);
+  }
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/layout/forms/moz.build
+++ b/layout/forms/moz.build
@@ -47,8 +47,17 @@ LOCAL_INCLUDES += [
     '../../content/html/content/src',
     '../../editor/libeditor/base',
     '../../editor/libeditor/text',
     '../../editor/txmgr/src',
     '../base',
     '../generic',
     '../xul',
 ]
+
+if CONFIG['ENABLE_INTL_API']:
+    # nsNumberControlFrame.cpp requires ICUUtils.h which in turn requires
+    # i18n/unum.h
+    LOCAL_INCLUDES += [
+        '../../intl/icu/source/common',
+        '../../intl/icu/source/i18n',
+    ]
+
--- a/layout/forms/nsNumberControlFrame.cpp
+++ b/layout/forms/nsNumberControlFrame.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsNumberControlFrame.h"
 
 #include "HTMLInputElement.h"
+#include "ICUUtils.h"
 #include "nsIFocusManager.h"
 #include "nsIPresShell.h"
 #include "nsFocusManager.h"
 #include "nsFontMetrics.h"
 #include "nsFormControlFrame.h"
 #include "nsGkAtoms.h"
 #include "nsINodeInfo.h"
 #include "nsINameSpaceManager.h"
@@ -279,17 +280,17 @@ nsNumberControlFrame::CreateAnonymousCon
                       NS_LITERAL_STRING("text"), PR_FALSE);
 
   HTMLInputElement* content = HTMLInputElement::FromContent(mContent);
   HTMLInputElement* textField = HTMLInputElement::FromContent(mTextField);
 
   // Initialize the text field value:
   nsAutoString value;
   content->GetValue(value);
-  mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::value, value, false);
+  SetValueOfAnonTextControl(value);
 
   // If we're readonly, make sure our anonymous text control is too:
   nsAutoString readonly;
   if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly)) {
     mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly, false);
   }
 
   // Propogate our tabindex:
@@ -522,35 +523,81 @@ void
 nsNumberControlFrame::AppendAnonymousContentTo(nsBaseContentList& aElements,
                                                uint32_t aFilter)
 {
   // Only one direct anonymous child:
   aElements.MaybeAppendElement(mOuterWrapper);
 }
 
 void
-nsNumberControlFrame::UpdateForValueChange(const nsAString& aValue)
+nsNumberControlFrame::SetValueOfAnonTextControl(const nsAString& aValue)
 {
   if (mHandlingInputEvent) {
     // We have been called while our HTMLInputElement is processing a DOM
     // 'input' event targeted at our anonymous text control. Our
     // HTMLInputElement has taken the value of our anon text control and
     // called SetValueInternal on itself to keep its own value in sync. As a
     // result SetValueInternal has called us. In this one case we do not want
     // to update our anon text control, especially since aValue will be the
     // sanitized value, and only the internal value should be sanitized (not
     // the value shown to the user, and certainly we shouldn't change it as
     // they type).
     return;
   }
+
+  // Init to aValue so that we set aValue as the value of our text control if
+  // aValue isn't a valid number (in which case the HTMLInputElement's validity
+  // state will be set to invalid) or if aValue can't be localized:
+  nsAutoString localizedValue(aValue);
+
+#ifdef ENABLE_INTL_API
+  // Try and localize the value we will set:
+  Decimal val = HTMLInputElement::StringToDecimal(aValue);
+  if (val.isFinite()) {
+    ICUUtils::LanguageTagIterForContent langTagIter(mContent);
+    ICUUtils::LocalizeNumber(val.toDouble(), langTagIter, localizedValue);
+  }
+#endif
+
   // We need to update the value of our anonymous text control here. Note that
   // this must be its value, and not its 'value' attribute (the default value),
   // since the default value is ignored once a user types into the text
   // control.
-  HTMLInputElement::FromContent(mTextField)->SetValue(aValue);
+  HTMLInputElement::FromContent(mTextField)->SetValue(localizedValue);
+}
+
+void
+nsNumberControlFrame::GetValueOfAnonTextControl(nsAString& aValue)
+{
+  if (!mTextField) {
+    aValue.Truncate();
+    return;
+  }
+
+  HTMLInputElement::FromContent(mTextField)->GetValue(aValue);
+
+#ifdef ENABLE_INTL_API
+  // Here we check if the text field's value is a localized serialization of a
+  // number. If it is we set aValue to the de-localize value, but only if the
+  // localized value isn't also a valid floating-point number according to the
+  // HTML 5 spec:
+  //
+  //   http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#floating-point-numbers
+  //
+  // This is because content (and tests) expect us to avoid "normalizing" the
+  // number that the user types in if it's not necessary. (E.g. if the user
+  // types "2e2" then inputElement.value should be "2e2" and not "100".
+  ICUUtils::LanguageTagIterForContent langTagIter(mContent);
+  double value = ICUUtils::ParseNumber(aValue, langTagIter);
+  if (NS_finite(value) &&
+      !HTMLInputElement::StringToDecimal(aValue).isFinite()) {
+    aValue.Truncate();
+    aValue.AppendFloat(value);
+  }
+#endif
 }
 
 Element*
 nsNumberControlFrame::GetPseudoElement(nsCSSPseudoElements::Type aType)
 {
   if (aType == nsCSSPseudoElements::ePseudo_mozNumberWrapper) {
     return mOuterWrapper;
   }
--- a/layout/forms/nsNumberControlFrame.h
+++ b/layout/forms/nsNumberControlFrame.h
@@ -74,20 +74,31 @@ public:
 
   virtual bool IsFrameOfType(uint32_t aFlags) const MOZ_OVERRIDE
   {
     return nsContainerFrame::IsFrameOfType(aFlags &
       ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock));
   }
 
   /**
-   * When our HTMLInputElement's value changes, it calls this method to tell
-   * us to sync up our anonymous text input field child.
+   * This method attempts to localizes aValue and then sets the result as the
+   * value of our anonymous text control. It's called when our
+   * HTMLInputElement's value changes, when we need to sync up the value
+   * displayed in our anonymous text control.
    */
-  void UpdateForValueChange(const nsAString& aValue);
+  void SetValueOfAnonTextControl(const nsAString& aValue);
+
+  /**
+   * This method gets the string value of our anonymous text control,
+   * attempts to normalizes (de-localizes) it, then sets the outparam aValue to
+   * the result. It's called when user input changes the text value of our
+   * anonymous text control so that we can sync up the internal value of our
+   * HTMLInputElement.
+   */
+  void GetValueOfAnonTextControl(nsAString& aValue);
 
   /**
    * Called to notify this frame that its HTMLInputElement is currently
    * processing a DOM 'input' event.
    */
   void HandlingInputEvent(bool aHandlingEvent)
   {
     mHandlingInputEvent = aHandlingEvent;