Bug 1344624 - [DateTimeInput] (l10n) Part 1: Display formatted numbers in <input type=time>. r=mossop draft
authorJessica Jong <jjong@mozilla.com>
Fri, 24 Mar 2017 11:13:20 +0800
changeset 504325 05918ba57513f9c816273a758ab2aa7198722135
parent 503306 7513b3f42058e9bcf9950d4acf4647d4ad2240f0
child 504326 afc5439ddff6d05a08bf111aaf5371be4cd1c640
push id50781
push userjjong@mozilla.com
push dateFri, 24 Mar 2017 06:29:23 +0000
reviewersmossop
bugs1344624
milestone55.0a1
Bug 1344624 - [DateTimeInput] (l10n) Part 1: Display formatted numbers in <input type=time>. r=mossop Add a new attribute "rawValue" in each of the numeric fields. We store the non-formatted number in this attribute, and display formatted number using Intl.NumberFormat. MozReview-Commit-ID: JkcBObFoYQ3
toolkit/content/widgets/datetimebox.xml
--- a/toolkit/content/widgets/datetimebox.xml
+++ b/toolkit/content/widgets/datetimebox.xml
@@ -701,18 +701,18 @@
               (this.isEmpty(millisecond) && this.mMillisecField)) {
             this.log("Edit fields need to be rebuilt.")
             this.reBuildEditFields();
           }
 
           this.setFieldValue(this.mHourField, hour);
           this.setFieldValue(this.mMinuteField, minute);
           if (this.mHour12) {
-            this.mDayPeriodField.value = (hour >= this.mMaxHour) ?
-              this.mPMIndicator : this.mAMIndicator;
+            this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
+                                                         : this.mAMIndicator);
           }
 
           if (!this.isEmpty(second)) {
             this.setFieldValue(this.mSecondField, second);
           }
           if (!this.isEmpty(millisecond)) {
             this.setFieldValue(this.mMillisecField, millisecond);
           }
@@ -737,36 +737,44 @@
               (this.mMillisecField && this.isEmpty(this.mMillisecField.value))) {
             // We still need to notify picker in case any of the field has
             // changed. If we can set input element value, then notifyPicker
             // will be called in setFieldsFromInputValue().
             this.notifyPicker();
             return;
           }
 
-          let hour = Number(this.mHourField.value);
+          let { hour, minute, second, millisecond } = this.getCurrentValue();
+
+          // Convert to a valid time string according to:
+          // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-time-string
           if (this.mHour12) {
-            let dayPeriod = this.mDayPeriodField.value;
+            let dayPeriod = this.getDayPeriodValue();
             if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
               hour += this.mMaxHour;
             } else if (dayPeriod == this.mAMIndicator &&
                        hour == this.mMaxHour) {
               hour = 0;
             }
           }
 
           hour = (hour < 10) ? ("0" + hour) : hour;
+          minute = (minute < 10) ? ("0" + minute) : minute;
 
-          let time = hour + ":" + this.mMinuteField.value;
+          let time = hour + ":" + minute;
           if (this.mSecondField) {
-            time += ":" + this.mSecondField.value;
+            second = (second < 10) ? ("0" + second) : second;
+            time += ":" + second;
           }
 
           if (this.mMillisecField) {
-            time += "." + this.mMillisecField.value;
+            // Convert milliseconds to fraction of second.
+            millisecond = millisecond.toString().padStart(
+              this.mMillisecMaxLength, "0");
+            time += "." + millisecond;
           }
 
           this.log("setInputValueFromFields: " + time);
           this.mInputElement.setUserInput(time);
         ]]>
         </body>
       </method>
 
@@ -776,19 +784,18 @@
         <![CDATA[
           let hour = aValue.hour;
           let minute = aValue.minute;
           this.log("setFieldsFromPicker: " + hour + ":" + minute);
 
           if (!this.isEmpty(hour)) {
             this.setFieldValue(this.mHourField, hour);
             if (this.mHour12) {
-              this.mDayPeriodField.value =
-                (hour >= this.mMaxHour) ? this.mPMIndicator
-                                        : this.mAMIndicator;
+              this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
+                                                           : this.mAMIndicator);
             }
           }
 
           if (!this.isEmpty(minute)) {
             this.setFieldValue(this.mMinuteField, minute);
           }
         ]]>
         </body>
@@ -801,61 +808,55 @@
           this.log("clearInputFields");
 
           if (this.isDisabled() || this.isReadonly()) {
             return;
           }
 
           if (this.mHourField && !this.mHourField.disabled &&
               !this.mHourField.readOnly) {
-            this.mHourField.value = "";
-            this.mHourField.setAttribute("typeBuffer", "");
+            this.clearFieldValue(this.mHourField);
           }
 
           if (this.mMinuteField && !this.mMinuteField.disabled &&
               !this.mMinuteField.readOnly) {
-            this.mMinuteField.value = "";
-            this.mMinuteField.setAttribute("typeBuffer", "");
+            this.clearFieldValue(this.mMinuteField);
           }
 
           if (this.mSecondField && !this.mSecondField.disabled &&
               !this.mSecondField.readOnly) {
-            this.mSecondField.value = "";
-            this.mSecondField.setAttribute("typeBuffer", "");
+            this.clearFieldValue(this.mSecondField);
           }
 
           if (this.mMillisecField && !this.mMillisecField.disabled &&
               !this.mMillisecField.readOnly) {
-            this.mMillisecField.value = "";
-            this.mMillisecField.setAttribute("typeBuffer", "");
+            this.clearFieldValue(this.mMillisecField);
           }
 
           if (this.mDayPeriodField && !this.mDayPeriodField.disabled &&
               !this.mDayPeriodField.readOnly) {
-            this.mDayPeriodField.value = "";
+            this.clearFieldValue(this.mDayPeriodField);
           }
 
           if (!aFromInputElement) {
             this.mInputElement.setUserInput("");
           }
-
-          this.updateResetButtonVisibility();
         ]]>
         </body>
       </method>
 
       <method name="incrementFieldValue">
         <parameter name="aTargetField"/>
         <parameter name="aTimes"/>
         <body>
         <![CDATA[
-          let value;
+          let value = this.getFieldValue(aTargetField);
 
           // Use current time if field is empty.
-          if (this.isEmpty(aTargetField.value)) {
+          if (this.isEmpty(value)) {
             let now = new Date();
 
             if (aTargetField == this.mHourField) {
               value = now.getHours();
               if (this.mHour12) {
                 value = (value % this.mMaxHour) || this.mMaxHour;
               }
             } else if (aTargetField == this.mMinuteField) {
@@ -863,31 +864,29 @@
             } else if (aTargetField == this.mSecondField) {
               value = now.getSeconds();
             } else if (aTargetField == this.mMillisecField) {
               value = now.getMilliseconds();
             } else {
               this.log("Field not supported in incrementFieldValue.");
               return;
             }
-          } else {
-            value = Number(aTargetField.value);
           }
 
           let min = aTargetField.getAttribute("min");
           let max = aTargetField.getAttribute("max");
 
           value += Number(aTimes);
           if (value > max) {
             value -= (max - min + 1);
           } else if (value < min) {
             value += (max - min + 1);
           }
+
           this.setFieldValue(aTargetField, value);
-          aTargetField.select();
         ]]>
         </body>
       </method>
 
       <method name="handleKeyboardNav">
         <parameter name="aEvent"/>
         <body>
         <![CDATA[
@@ -900,21 +899,19 @@
 
           if (this.mDayPeriodField &&
               targetField == this.mDayPeriodField) {
             // Home/End key does nothing on AM/PM field.
             if (key == "Home" || key == "End") {
               return;
             }
 
-            this.mDayPeriodField.value =
-              this.mDayPeriodField.value == this.mAMIndicator ?
-                this.mPMIndicator : this.mAMIndicator;
-            this.mDayPeriodField.select();
-            this.updateResetButtonVisibility();
+            this.setDayPeriodValue(
+              this.getDayPeriodValue() == this.mAMIndicator ? this.mPMIndicator
+                                                            : this.mAMIndicator);
             this.setInputValueFromFields();
             return;
           }
 
           switch (key) {
             case "ArrowUp":
               this.incrementFieldValue(targetField, 1);
               break;
@@ -929,22 +926,20 @@
             case "PageDown": {
               let interval = targetField.getAttribute("pginterval");
               this.incrementFieldValue(targetField, 0 - interval);
               break;
             }
             case "Home":
               let min = targetField.getAttribute("min");
               this.setFieldValue(targetField, min);
-              targetField.select();
               break;
             case "End":
               let max = targetField.getAttribute("max");
               this.setFieldValue(targetField, max);
-              targetField.select();
               break;
           }
           this.setInputValueFromFields();
         ]]>
         </body>
       </method>
 
       <method name="handleKeypress">
@@ -956,32 +951,28 @@
           }
 
           let targetField = aEvent.originalTarget;
           let key = aEvent.key;
 
           if (this.mDayPeriodField &&
               targetField == this.mDayPeriodField) {
             if (key == "a" || key == "A") {
-              this.mDayPeriodField.value = this.mAMIndicator;
-              this.mDayPeriodField.select();
+              this.setDayPeriodValue(this.mAMIndicator);
             } else if (key == "p" || key == "P") {
-              this.mDayPeriodField.value = this.mPMIndicator;
-              this.mDayPeriodField.select();
+              this.setDayPeriodValue(this.mPMIndicator);
             }
-            this.updateResetButtonVisibility();
             return;
           }
 
           if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
             let buffer = targetField.getAttribute("typeBuffer") || "";
 
             buffer = buffer.concat(key);
             this.setFieldValue(targetField, buffer);
-            targetField.select();
 
             let n = Number(buffer);
             let max = targetField.getAttribute("max");
             if (buffer.length >= targetField.maxLength || n * 10 > max) {
               buffer = "";
               this.advanceToNextField();
             }
             targetField.setAttribute("typeBuffer", buffer);
@@ -990,102 +981,135 @@
         </body>
       </method>
 
       <method name="setFieldValue">
        <parameter name="aField"/>
        <parameter name="aValue"/>
         <body>
         <![CDATA[
+          if (!aField || !aField.classList.contains("numeric")) {
+            return;
+          }
+
           let value = Number(aValue);
           if (isNaN(value)) {
             this.log("NaN on setFieldValue!");
             return;
           }
 
-          if (aField.maxLength == this.mMaxLength) { // For hour, minute and second
-            if (aField == this.mHourField) {
-              if (this.mHour12) {
-                // Try to change to 12hr format if user input is 0 or greater
-                // than 12.
-                if (value == 0 && aValue.length == aField.maxLength) {
-                  value = this.mMaxHour;
-                } else {
-                  value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
-                }
-              } else if (value > this.mMaxHour) {
+          if (aField == this.mHourField) {
+            if (this.mHour12) {
+              // Try to change to 12hr format if user input is 0 or greater
+              // than 12.
+              if (value == 0 && aValue.length == aField.maxLength) {
                 value = this.mMaxHour;
+              } else {
+                value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
               }
-            }
-            // prepend zero
-            if (value < 10) {
-              value = "0" + value;
-            }
-          } else if (aField.maxLength == this.mMillisecMaxLength) {
-            // prepend zeroes
-            if (value < 10) {
-              value = "00" + value;
-            } else if (value < 100) {
-              value = "0" + value;
+            } else if (value > this.mMaxHour) {
+              value = this.mMaxHour;
             }
           }
 
-          aField.value = value;
+          aField.setAttribute("rawValue", value);
+
+          let minDigits = aField.size;
+          let formatted = value.toLocaleString(this.mLocales, {
+            minimumIntegerDigits: minDigits,
+            useGrouping: false
+          });
+
+          aField.value = formatted;
+          if (document.activeElement == this.mInputElement &&
+              this.mLastFocusedField && this.mLastFocusedField == aField) {
+            aField.select();
+          }
+          this.updateResetButtonVisibility();
+        ]]>
+        </body>
+      </method>
+
+      <method name="getDayPeriodValue">
+        <parameter name="aValue"/>
+        <body>
+        <![CDATA[
+          if (!this.mDayPeriodField) {
+            return "";
+          }
+
+          return this.mDayPeriodField.value;
+        ]]>
+        </body>
+      </method>
+
+      <method name="setDayPeriodValue">
+        <parameter name="aValue"/>
+        <body>
+        <![CDATA[
+          if (!this.mDayPeriodField) {
+            return;
+          }
+
+          this.mDayPeriodField.value = aValue;
+          if (document.activeElement == this.mInputElement &&
+              this.mLastFocusedField &&
+              this.mLastFocusedField == this.mDayPeriodField) {
+            this.mDayPeriodField.select();
+          }
           this.updateResetButtonVisibility();
         ]]>
         </body>
       </method>
 
       <method name="isAnyValueAvailable">
         <parameter name="aForPicker"/>
         <body>
         <![CDATA[
-          let available = !this.isEmpty(this.mHourField.value) ||
-                          !this.isEmpty(this.mMinuteField.value);
+          let { hour, minute, second, millisecond } = this.getCurrentValue();
+          let dayPeriod = this.getDayPeriodValue();
 
+          let available = !this.isEmpty(hour) || !this.isEmpty(minute);
           if (available) {
             return true;
           }
 
           // Picker only cares about hour:minute.
           if (aForPicker) {
             return false;
           }
 
-          return (this.mDayPeriodField && !this.isEmpty(this.mDayPeriodField.value)) ||
-                 (this.mSecondField && !this.isEmpty(this.mSecondField.value)) ||
-                 (this.mMillisecField && !this.isEmpty(this.mMillisecField.value));
+          return (this.mDayPeriodField && !this.isEmpty(dayPeriod)) ||
+                 (this.mSecondField && !this.isEmpty(second)) ||
+                 (this.mMillisecField && !this.isEmpty(millisecond));
         ]]>
         </body>
       </method>
 
       <method name="getCurrentValue">
         <body>
         <![CDATA[
-          let hour;
-          if (!this.isEmpty(this.mHourField.value)) {
-            hour = Number(this.mHourField.value);
+          let hour = this.getFieldValue(this.mHourField);
+          if (!this.isEmpty(hour)) {
             if (this.mHour12) {
-              let dayPeriod = this.mDayPeriodField.value;
+              let dayPeriod = this.getDayPeriodValue();
               if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
                 hour += this.mMaxHour;
               } else if (dayPeriod == this.mAMIndicator &&
                          hour == this.mMaxHour) {
                 hour = 0;
               }
             }
-           }
-
-          let minute;
-          if (!this.isEmpty(this.mMinuteField.value)) {
-            minute = Number(this.mMinuteField.value);
           }
 
-          // Picker only needs hour/minute.
-          let time = { hour, minute };
+          let minute = this.getFieldValue(this.mMinuteField);
+          let second = this.getFieldValue(this.mSecondField);
+          let millisecond = this.getFieldValue(this.mMillisecField);
+
+          let time = { hour, minute, second, millisecond };
 
           this.log("getCurrentValue: " + JSON.stringify(time));
           return time;
         ]]>
         </body>
       </method>
     </implementation>
   </binding>
@@ -1185,23 +1209,31 @@
           field.classList.add("textbox-input", "datetime-input");
           field.disabled = this.mInputElement.disabled;
           field.readOnly = this.mInputElement.readOnly;
           field.tabIndex = this.mInputElement.tabIndex;
           field.placeholder = aPlaceHolder;
 
           field.setAttribute("size", aLength);
           field.setAttribute("maxlength", aLength);
-          field.setAttribute("typeBuffer", "");
 
           if (aIsNumeric) {
             field.classList.add("numeric");
+            // Maximum value allowed.
             field.setAttribute("min", aMin);
+            // Minumim value allowed.
             field.setAttribute("max", aMax);
+            // Interval when pressing pageUp/pageDown key.
             field.setAttribute("pginterval", aPageUpDownInterval);
+            // Used to store what the user has already typed in the field,
+            // cleared when value is cleared and when field is blurred.
+            field.setAttribute("typeBuffer", "");
+            // Used to store the non-formatted number, clered when value is
+            // cleared.
+            field.setAttribute("rawValue", "");
           }
 
           return field;
         ]]>
         </body>
       </method>
 
       <method name="updateResetButtonVisibility">
@@ -1362,16 +1394,51 @@
           let str = aString.replace(
             /[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, "");
 
           return str;
         ]]>
         </body>
       </method>
 
+      <method name="getFieldValue">
+        <parameter name="aField"/>
+        <body>
+        <![CDATA[
+          if (!aField || !aField.classList.contains("numeric")) {
+            return undefined;
+          }
+
+          let value = aField.getAttribute("rawValue");
+          // Avoid returning 0 when field is empty.
+          return (this.isEmpty(value) ? undefined : Number(value));
+        ]]>
+        </body>
+      </method>
+
+      <method name="clearFieldValue">
+        <parameter name="aField"/>
+        <body>
+        <![CDATA[
+          aField.value = "";
+          if (aField.classList.contains("numeric")) {
+            aField.setAttribute("typeBuffer", "");
+            aField.setAttribute("rawValue", "");
+          }
+          this.updateResetButtonVisibility();
+        ]]>
+        </body>
+      </method>
+
+      <method name="setFieldValue">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
       <method name="clearInputFields">
         <body>
           throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
         </body>
       </method>
 
       <method name="setFieldsFromInputValue">
         <body>
@@ -1516,19 +1583,17 @@
             case "Enter":
             case " ": {
               this.mInputElement.closeDateTimePicker();
               aEvent.preventDefault();
               break;
             }
             case "Backspace": {
               let targetField = aEvent.originalTarget;
-              targetField.value = "";
-              targetField.setAttribute("typeBuffer", "");
-              this.updateResetButtonVisibility();
+              this.clearFieldValue(targetField);
               this.setInputValueFromFields();
               aEvent.preventDefault();
               break;
             }
             case "ArrowRight":
             case "ArrowLeft": {
               this.advanceToNextField(aEvent.key == "ArrowRight" ? false : true);
               aEvent.preventDefault();