--- a/toolkit/content/widgets/datetimebox.xml
+++ b/toolkit/content/widgets/datetimebox.xml
@@ -468,115 +468,161 @@
this.mHour12 = true;
this.mSeparatorText = ":";
this.mMillisecSeparatorText = ".";
this.mMaxLength = 2;
this.mMillisecMaxLength = 3;
this.mDefaultStep = 60 * 1000; // in milliseconds
- this.mMinHourInHour12 = 1;
- this.mMaxHourInHour12 = 12;
+ this.mMinHour = this.mHour12 ? 1 : 0;
+ this.mMaxHour = this.mHour12 ? 12 : 23;
this.mMinMinute = 0;
this.mMaxMinute = 59;
this.mMinSecond = 0;
this.mMaxSecond = 59;
this.mMinMillisecond = 0;
this.mMaxMillisecond = 999;
this.mHourPageUpDownInterval = 3;
this.mMinSecPageUpDownInterval = 10;
- this.mHourField =
- document.getAnonymousElementByAttribute(this, "anonid", "input-one");
- this.mHourField.setAttribute("typeBuffer", "");
- this.mMinuteField =
- document.getAnonymousElementByAttribute(this, "anonid", "input-two");
- this.mMinuteField.setAttribute("typeBuffer", "");
- this.mDayPeriodField =
- document.getAnonymousElementByAttribute(this, "anonid", "input-three");
- this.mDayPeriodField.classList.remove("numeric");
-
- let dayPeriodLength =
- Math.max(this.mAMIndicator.length, this.mPMIndicator.length);
-
- this.mDayPeriodField.size = dayPeriodLength;
- this.mDayPeriodField.maxLength = dayPeriodLength;
-
- this.mHourField.placeholder = this.mHourPlaceHolder;
- this.mMinuteField.placeholder = this.mMinutePlaceHolder;
- this.mDayPeriodField.placeholder = this.mDayPeriodPlaceHolder;
-
- this.mHourField.setAttribute("min", this.mMinHourInHour12);
- this.mHourField.setAttribute("max", this.mMaxHourInHour12);
- this.mMinuteField.setAttribute("min", this.mMinMinute);
- this.mMinuteField.setAttribute("max", this.mMaxMinute);
-
- this.mMinuteSeparator =
- document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
- this.mMinuteSeparator.textContent = this.mSeparatorText;
- this.mSpaceSeparator =
- document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
- // space between time and am/pm field
- this.mSpaceSeparator.textContent = " ";
-
- this.mSecondSeparator = null;
- this.mSecondField = null;
- this.mMillisecSeparator = null;
- this.mMillisecField = null;
+ this.buildEditFields();
if (this.mInputElement.value) {
this.setFieldsFromInputValue();
}
this.updateResetButtonVisibility();
]]>
</constructor>
- <method name="insertSeparator">
- <parameter name="aSeparatorText"/>
+ <method name="getInputElementValues">
<body>
<![CDATA[
- let container = this.mHourField.parentNode;
- const HTML_NS = "http://www.w3.org/1999/xhtml";
+ let value = this.mInputElement.value;
+ if (value.length === 0) {
+ return {};
+ }
+
+ let hour, minute, second, millisecond;
+ [hour, minute, second] = value.split(":");
+ if (second) {
+ [second, millisecond] = second.split(".");
- let separator = document.createElementNS(HTML_NS, "span");
- separator.textContent = aSeparatorText;
- separator.setAttribute("class", "datetime-separator");
- container.insertBefore(separator, this.mSpaceSeparator);
+ // Convert fraction of second to milliseconds.
+ if (millisecond && millisecond.length === 1) {
+ millisecond *= 100;
+ } else if (millisecond && millisecond.length === 2) {
+ millisecond *= 10;
+ }
+ }
- return separator;
+ return { hour, minute, second, millisecond };
]]>
</body>
</method>
- <method name="insertAdditionalField">
- <parameter name="aPlaceHolder"/>
- <parameter name="aMin"/>
- <parameter name="aMax"/>
- <parameter name="aSize"/>
- <parameter name="aMaxLength"/>
+ <method name="reBuildEditFields">
+ <body>
+ <![CDATA[
+ let root =
+ document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
+ while (root.firstChild) {
+ root.firstChild.remove();
+ }
+
+ this.mHourField = null;
+ this.mMinuteField = null;
+ this.mSecondField = null;
+ this.mMillisecField = null;
+
+ this.buildEditFields();
+ ]]>
+ </body>
+ </method>
+
+ <method name="buildEditFields">
<body>
<![CDATA[
- let container = this.mHourField.parentNode;
const HTML_NS = "http://www.w3.org/1999/xhtml";
+ let root =
+ document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
+
+ let { second, millisecond } = this.getInputElementValues();
+
+ let options = {
+ hour: "numeric",
+ minute: "numeric",
+ hour12: this.mHour12
+ };
+
+ this.mHourField = this.createEditField(this.mHourPlaceHolder,
+ this.mMaxLength, true, this.mMinHour, this.mMaxHour,
+ this.mHourPageUpDownInterval);
+ this.mMinuteField = this.createEditField(this.mMinutePlaceHolder,
+ this.mMaxLength, true, this.mMinMinute, this.mMaxMinute,
+ this.mMinSecPageUpDownInterval);
+
+ if (this.mHour12) {
+ let dayPeriodLength =
+ Math.max(this.mAMIndicator.length, this.mPMIndicator.length);
+
+ this.mDayPeriodField = this.createEditField(
+ this.mDayPeriodPlaceHolder, dayPeriodLength, false);
+ }
+
+ if (second != undefined) {
+ options["second"] = "numeric";
+ this.mSecondField = this.createEditField(this.mSecondPlaceHolder,
+ this.mMaxLength, true, this.mMinSecond, this.mMaxSecond,
+ this.mMinSecPageUpDownInterval);
+ }
- let field = document.createElementNS(HTML_NS, "input");
- field.classList.add("textbox-input", "datetime-input", "numeric");
- field.setAttribute("size", aSize);
- field.setAttribute("maxlength", aMaxLength);
- field.setAttribute("min", aMin);
- field.setAttribute("max", aMax);
- field.setAttribute("typeBuffer", "");
- field.disabled = this.mInputElement.disabled;
- field.readOnly = this.mInputElement.readOnly;
- field.tabIndex = this.mInputElement.tabIndex;
- field.placeholder = aPlaceHolder;
- container.insertBefore(field, this.mSpaceSeparator);
+ if (millisecond != undefined) {
+ this.mMillisecField = this.createEditField(this.mMillisecPlaceHolder,
+ this.mMillisecMaxLength, true, this.mMinMillisecond,
+ this.mMaxMillisecond, this.mMinSecPageUpDownInterval);
+ }
- return field;
+ let fragment = document.createDocumentFragment();
+ let formatter = Intl.DateTimeFormat(this.mLocales, options);
+ formatter.formatToParts(Date.now()).map(part => {
+ switch (part.type) {
+ case "hour":
+ fragment.appendChild(this.mHourField);
+ break;
+ case "minute":
+ fragment.appendChild(this.mMinuteField);
+ break;
+ case "second":
+ fragment.appendChild(this.mSecondField);
+ if (millisecond != undefined) {
+ // Intl.DateTimeFormat does not support millisecond, so we
+ // need to handle this on our own.
+ let span = document.createElementNS(HTML_NS, "span");
+ span.textContent = this.mMillisecSeparatorText;
+ fragment.appendChild(span);
+ fragment.appendChild(this.mMillisecField);
+ }
+ break;
+ case "dayPeriod":
+ fragment.appendChild(this.mDayPeriodField);
+ break;
+ default:
+ // Remove bidirectional formatting characters, we'll handle
+ // directions on our own.
+ let value = this.stripDirFormattingChars(part.value);
+ let span = document.createElementNS(HTML_NS, "span");
+ span.textContent = value;
+ fragment.appendChild(span);
+ break;
+ }
+ });
+
+ root.appendChild(fragment);
]]>
</body>
</method>
<method name="getStringsForLocale">
<parameter name="aLocales"/>
<body>
<![CDATA[
@@ -601,82 +647,48 @@
return { amString, pmString };
]]>
</body>
</method>
<method name="setFieldsFromInputValue">
<body>
<![CDATA[
- let value = this.mInputElement.value;
- if (!value) {
+ let { hour, minute, second, millisecond } =
+ this.getInputElementValues();
+
+ if (this.isEmpty(hour) && this.isEmpty(minute)) {
this.clearInputFields(true);
return;
}
- this.log("setFieldsFromInputValue: " + value);
- let [hour, minute, second] = value.split(":");
+ // Second and millsecond part is optional, rebuild edit fields if
+ // needed.
+ if ((!this.isEmpty(second) && !this.mSecondField) ||
+ (this.isEmpty(second) && this.mSecondField) ||
+ (!this.isEmpty(millisecond) && !this.mMillisecField) ||
+ (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.mMaxHourInHour12) ?
+ this.mDayPeriodField.value = (hour >= this.mMaxHour) ?
this.mPMIndicator : this.mAMIndicator;
}
if (!this.isEmpty(second)) {
- let index = second.indexOf(".");
- let millisecond;
- if (index != -1) {
- millisecond = second.substring(index + 1);
- second = second.substring(0, index);
- }
-
- if (!this.mSecondField) {
- this.mSecondSeparator = this.insertSeparator(this.mSeparatorText);
- this.mSecondField = this.insertAdditionalField(
- this.mSecondPlaceHolder, this.mMinSecond, this.mMaxSecond,
- this.mMaxLength, this.mMaxLength);
- }
this.setFieldValue(this.mSecondField, second);
+ }
+ if (!this.isEmpty(millisecond)) {
+ this.setFieldValue(this.mMillisecField, millisecond);
+ }
- if (!this.isEmpty(millisecond)) {
- if (!this.mMillisecField) {
- this.mMillisecSeparator = this.insertSeparator(
- this.mMillisecSeparatorText);
- this.mMillisecField = this.insertAdditionalField(
- this.mMillisecPlaceHolder, this.mMinMillisecond,
- this.mMaxMillisecond, this.mMillisecMaxLength,
- this.mMillisecMaxLength);
- }
- this.setFieldValue(this.mMillisecField, millisecond);
- } else if (this.mMillisecField) {
- this.mMillisecField.remove();
- this.mMillisecField = null;
-
- this.mMillisecSeparator.remove();
- this.mMillisecSeparator = null;
- }
- } else {
- if (this.mSecondField) {
- this.mSecondField.remove();
- this.mSecondField = null;
-
- this.mSecondSeparator.remove();
- this.mSecondSeparator = null;
- }
-
- if (this.mMillisecField) {
- this.mMillisecField.remove();
- this.mMillisecField = null;
-
- this.mMillisecSeparator.remove();
- this.mMillisecSeparator = null;
- }
- }
this.notifyPicker();
]]>
</body>
</method>
<method name="setInputValueFromFields">
<body>
<![CDATA[
@@ -695,21 +707,20 @@
// will be called in setFieldsFromInputValue().
this.notifyPicker();
return;
}
let hour = Number(this.mHourField.value);
if (this.mHour12) {
let dayPeriod = this.mDayPeriodField.value;
- if (dayPeriod == this.mPMIndicator &&
- hour < this.mMaxHourInHour12) {
- hour += this.mMaxHourInHour12;
+ if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
+ hour += this.mMaxHour;
} else if (dayPeriod == this.mAMIndicator &&
- hour == this.mMaxHourInHour12) {
+ hour == this.mMaxHour) {
hour = 0;
}
}
hour = (hour < 10) ? ("0" + hour) : hour;
let time = hour + ":" + this.mMinuteField.value;
if (this.mSecondField) {
@@ -733,18 +744,18 @@
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.mMaxHourInHour12) ? this.mPMIndicator
- : this.mAMIndicator;
+ (hour >= this.mMaxHour) ? this.mPMIndicator
+ : this.mAMIndicator;
}
}
if (!this.isEmpty(minute)) {
this.setFieldValue(this.mMinuteField, minute);
}
]]>
</body>
@@ -805,18 +816,19 @@
<![CDATA[
let value;
// Use current time if field is empty.
if (this.isEmpty(aTargetField.value)) {
let now = new Date();
if (aTargetField == this.mHourField) {
- value = now.getHours() % this.mMaxHourInHour12 ||
- this.mMaxHourInHour12;
+ if (this.mHour12) {
+ value = now.getHours() % this.mMaxHour || this.mMaxHour;
+ }
} else if (aTargetField == this.mMinuteField) {
value = now.getMinutes();
} else if (aTargetField == this.mSecondField) {
value = now.getSeconds();
} else if (aTargetField == this.mMillisecField) {
value = now.getMilliseconds();
} else {
this.log("Field not supported in incrementFieldValue.");
@@ -824,17 +836,17 @@
}
} else {
value = Number(aTargetField.value);
}
let min = aTargetField.getAttribute("min");
let max = aTargetField.getAttribute("max");
- value += aTimes;
+ value += Number(aTimes);
if (value > max) {
value -= (max - min + 1);
} else if (value < min) {
value += (max - min + 1);
}
this.setFieldValue(aTargetField, value);
aTargetField.select();
]]>
@@ -870,26 +882,26 @@
switch (key) {
case "ArrowUp":
this.incrementFieldValue(targetField, 1);
break;
case "ArrowDown":
this.incrementFieldValue(targetField, -1);
break;
- case "PageUp":
- this.incrementFieldValue(targetField,
- targetField == this.mHourField ? this.mHourPageUpDownInterval
- : this.mMinSecPageUpDownInterval);
+ case "PageUp": {
+ let interval = targetField.getAttribute("pginterval");
+ this.incrementFieldValue(targetField, interval);
break;
- case "PageDown":
- this.incrementFieldValue(targetField,
- targetField == this.mHourField ? (0 - this.mHourPageUpDownInterval)
- : (0 - this.mMinSecPageUpDownInterval));
+ }
+ 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);
@@ -952,20 +964,21 @@
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 && this.mHour12) {
- value = (value > this.mMaxHourInHour12) ?
- value - this.mMaxHourInHour12 : value;
if (aValue == "00") {
- value = this.mMaxHourInHour12;
+ value = this.mMaxHour;
+ } else {
+ // Change to 12hr format if user input is greater than 12.
+ value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
}
}
// prepend zero
if (value < 10) {
value = "0" + value;
}
} else if (aField.maxLength == this.mMillisecMaxLength) {
// prepend zeroes
@@ -1008,21 +1021,20 @@
<method name="getCurrentValue">
<body>
<![CDATA[
let hour;
if (!this.isEmpty(this.mHourField.value)) {
hour = Number(this.mHourField.value);
if (this.mHour12) {
let dayPeriod = this.mDayPeriodField.value;
- if (dayPeriod == this.mPMIndicator &&
- hour < this.mMaxHourInHour12) {
- hour += this.mMaxHourInHour12;
+ if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
+ hour += this.mMaxHour;
} else if (dayPeriod == this.mAMIndicator &&
- hour == this.mMaxHourInHour12) {
+ hour == this.mMaxHour) {
hour = 0;
}
}
}
let minute;
if (!this.isEmpty(this.mMinuteField.value)) {
minute = Number(this.mMinuteField.value);
@@ -1044,31 +1056,20 @@
<stylesheet src="chrome://global/content/textbox.css"/>
<stylesheet src="chrome://global/skin/textbox.css"/>
<stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
</resources>
<content>
<html:div class="datetime-input-box-wrapper"
xbl:inherits="context,disabled,readonly">
- <html:span>
- <html:input anonid="input-one"
- class="textbox-input datetime-input numeric"
- size="2" maxlength="2"
- xbl:inherits="disabled,readonly,tabindex"/>
- <html:span anonid="sep-first" class="datetime-separator"></html:span>
- <html:input anonid="input-two"
- class="textbox-input datetime-input numeric"
- size="2" maxlength="2"
- xbl:inherits="disabled,readonly,tabindex"/>
- <html:span anonid="sep-second" class="datetime-separator"></html:span>
- <html:input anonid="input-three"
- class="textbox-input datetime-input numeric"
- size="2" maxlength="2"
- xbl:inherits="disabled,readonly,tabindex"/>
+ <html:span class="datetime-input-edit-wrapper"
+ anonid="edit-wrapper">
+ <!-- Each of the date/time input types will append their input child
+ - elements here -->
</html:span>
<html:button class="datetime-reset-button" anonid="reset-button"
tabindex="-1" xbl:inherits="disabled"/>
</html:div>
</content>
<implementation implements="nsIDateTimeInputArea">
@@ -1125,33 +1126,76 @@
<![CDATA[
if (this.DEBUG) {
dump("[DateTimeBox] " + aMsg + "\n");
}
]]>
</body>
</method>
+ <method name="createEditField">
+ <parameter name="aPlaceHolder"/>
+ <parameter name="aLength"/>
+ <parameter name="aIsNumeric"/>
+ <parameter name="aMin"/>
+ <parameter name="aMax"/>
+ <parameter name="aPageUpDownInterval"/>
+ <body>
+ <![CDATA[
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+ let field = document.createElementNS(HTML_NS, "input");
+ 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");
+ field.setAttribute("min", aMin);
+ field.setAttribute("max", aMax);
+ field.setAttribute("pginterval", aPageUpDownInterval);
+ }
+
+ return field;
+ ]]>
+ </body>
+ </method>
+
<method name="updateResetButtonVisibility">
<body>
<![CDATA[
if (this.isAnyValueAvailable(false)) {
this.mResetButton.style.visibility = "visible";
} else {
this.mResetButton.style.visibility = "hidden";
}
]]>
</body>
</method>
<method name="focusInnerTextBox">
<body>
<![CDATA[
this.log("focusInnerTextBox");
- document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus();
+
+ let editRoot =
+ document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
+
+ for (let child = editRoot.firstChild; child; child = child.nextSibling) {
+ if (child instanceof HTMLInputElement) {
+ child.focus();
+ break;
+ }
+ }
]]>
</body>
</method>
<method name="blurInnerTextBox">
<body>
<![CDATA[
this.log("blurInnerTextBox");
@@ -1218,16 +1262,30 @@
<method name="isEmpty">
<parameter name="aValue"/>
<body>
return (aValue == undefined || 0 === aValue.length);
</body>
</method>
+ <method name="stripDirFormattingChars">
+ <parameter name="aString"/>
+ <body>
+ <![CDATA[
+ // Strip bidirectional formatting characters.
+ // http://www.unicode.org/reports/tr9/#Directional_Formatting_Characters
+ let str = aString.replace(
+ /[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, "");
+
+ return str;
+ ]]>
+ </body>
+ </method>
+
<method name="clearInputFields">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="setFieldsFromInputValue">
<body>