summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjanekptacijarabaci <janekptacijarabaci@seznam.cz>2018-03-30 12:17:17 +0200
committerjanekptacijarabaci <janekptacijarabaci@seznam.cz>2018-03-30 12:17:17 +0200
commite25430117a67f5c898e5e9388ebd44b185d469ab (patch)
tree2b625f7778dfab0b5219bf6a868ab393eb5be9cd
parenta1a007a4856fa50d6d811c2268f881e3666f4c67 (diff)
downloadUXP-e25430117a67f5c898e5e9388ebd44b185d469ab.tar
UXP-e25430117a67f5c898e5e9388ebd44b185d469ab.tar.gz
UXP-e25430117a67f5c898e5e9388ebd44b185d469ab.tar.lz
UXP-e25430117a67f5c898e5e9388ebd44b185d469ab.tar.xz
UXP-e25430117a67f5c898e5e9388ebd44b185d469ab.zip
moebius#92: HTML - input - datetime
+ native in moebius: Bug 1317600: https://bugzilla.mozilla.org/show_bug.cgi?id=1317600 A note - not implemented: Bug 1282768: https://bugzilla.mozilla.org/show_bug.cgi?id=1282768 *.css: filter: url("chrome://global/skin/filters.svg#fill");, fill: Bug 1283385: https://bugzilla.mozilla.org/show_bug.cgi?id=1283385 Bug 1323109: https://bugzilla.mozilla.org/show_bug.cgi?id=1323109 Bug 1314544: https://bugzilla.mozilla.org/show_bug.cgi?id=1314544 Bug 1286182: https://bugzilla.mozilla.org/show_bug.cgi?id=1286182 Bug 1325922: https://bugzilla.mozilla.org/show_bug.cgi?id=1325922 A note - not implemented: Bug 1282768: https://bugzilla.mozilla.org/show_bug.cgi?id=1282768 *.css: filter: url("chrome://global/skin/filters.svg#fill");, fill: Bug 1320225: https://bugzilla.mozilla.org/show_bug.cgi?id=1320225 Bug 1341190: https://bugzilla.mozilla.org/show_bug.cgi?id=1341190
-rw-r--r--browser/base/content/browser.css2
-rw-r--r--browser/base/content/browser.xul4
-rw-r--r--dom/html/HTMLInputElement.cpp185
-rw-r--r--dom/html/HTMLInputElement.h66
-rw-r--r--dom/html/test/forms/mochitest.ini2
-rw-r--r--dom/html/test/forms/test_input_date_key_events.html8
-rw-r--r--dom/html/test/forms/test_input_datetime_input_change_events.html88
-rw-r--r--layout/reftests/forms/input/datetime/reftest.list11
-rw-r--r--layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html9
-rw-r--r--layout/reftests/forms/input/datetime/time-content-left-aligned.html9
-rw-r--r--layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html9
-rw-r--r--layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html10
-rw-r--r--layout/reftests/forms/input/datetime/time-small-height-ref.html18
-rw-r--r--layout/reftests/forms/input/datetime/time-small-height.html19
-rw-r--r--layout/reftests/forms/input/datetime/time-small-width-height-ref.html18
-rw-r--r--layout/reftests/forms/input/datetime/time-small-width-height.html19
-rw-r--r--layout/reftests/forms/input/datetime/time-small-width-ref.html19
-rw-r--r--layout/reftests/forms/input/datetime/time-small-width.html20
-rw-r--r--layout/style/res/forms.css5
-rw-r--r--toolkit/content/tests/browser/browser.ini1
-rw-r--r--toolkit/content/tests/browser/browser_datetime_datepicker.js222
-rw-r--r--toolkit/content/tests/browser/head.js85
-rw-r--r--toolkit/content/widgets/calendar.js22
-rw-r--r--toolkit/content/widgets/datekeeper.js171
-rw-r--r--toolkit/content/widgets/datepicker.js94
-rw-r--r--toolkit/content/widgets/datetimebox.css10
-rw-r--r--toolkit/content/widgets/datetimebox.xml156
-rw-r--r--toolkit/content/widgets/datetimepicker.xml6
-rw-r--r--toolkit/content/widgets/datetimepopup.xml101
-rw-r--r--toolkit/content/widgets/spinner.js4
-rw-r--r--toolkit/locales/en-US/chrome/global/datetimebox.dtd9
-rw-r--r--toolkit/locales/jar.mn1
-rw-r--r--toolkit/modules/DateTimePickerHelper.jsm22
-rw-r--r--toolkit/themes/shared/datetimeinputpickers.css90
34 files changed, 1267 insertions, 248 deletions
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index 7557d15a1..f03f21c3f 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -557,7 +557,7 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks {
transition: none;
}
-#DateTimePickerPanel {
+#DateTimePickerPanel[active="true"] {
-moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup");
}
diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul
index ae531e167..62c2b122f 100644
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -160,8 +160,10 @@
hidden="true"
orient="vertical"
noautofocus="true"
+ noautohide="true"
consumeoutsideclicks="false"
- level="parent">
+ level="parent"
+ tabspecific="true">
<iframe id="dateTimePopupFrame"/>
</panel>
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
index e8a4af454..62f194493 100644
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -543,9 +543,8 @@ GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData,
bool
HTMLInputElement::ValueAsDateEnabled(JSContext* cx, JSObject* obj)
{
- return Preferences::GetBool("dom.experimental_forms", false) ||
- Preferences::GetBool("dom.forms.datepicker", false) ||
- Preferences::GetBool("dom.forms.datetime", false);
+ return IsExperimentalFormsEnabled() || IsDatePickerEnabled() ||
+ IsInputDateTimeEnabled();
}
NS_IMETHODIMP
@@ -628,7 +627,7 @@ HTMLInputElement::nsFilePickerShownCallback::Done(int16_t aResult)
RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
new DispatchChangeEventCallback(mInput);
- if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ if (IsWebkitDirPickerEnabled() &&
mInput->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
ErrorResult error;
GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error);
@@ -827,7 +826,7 @@ HTMLInputElement::IsPopupBlocked() const
nsresult
HTMLInputElement::InitDatePicker()
{
- if (!Preferences::GetBool("dom.forms.datepicker", false)) {
+ if (!IsDatePickerEnabled()) {
return NS_OK;
}
@@ -2516,10 +2515,8 @@ bool
HTMLInputElement::IsExperimentalMobileType(uint8_t aType)
{
return (aType == NS_FORM_INPUT_DATE &&
- !Preferences::GetBool("dom.forms.datetime", false) &&
- !Preferences::GetBool("dom.forms.datepicker", false)) ||
- (aType == NS_FORM_INPUT_TIME &&
- !Preferences::GetBool("dom.forms.datetime", false));
+ !IsInputDateTimeEnabled() && !IsDatePickerEnabled()) ||
+ (aType == NS_FORM_INPUT_TIME && !IsInputDateTimeEnabled());
}
bool
@@ -3025,8 +3022,8 @@ HTMLInputElement::GetDisplayFileName(nsAString& aValue) const
nsXPIDLString value;
if (mFilesOrDirectories.IsEmpty()) {
- if ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) ||
- (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ if ((IsDirPickerEnabled() && Allowdirs()) ||
+ (IsWebkitDirPickerEnabled() &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
"NoDirSelected", value);
@@ -3055,7 +3052,7 @@ HTMLInputElement::SetFilesOrDirectories(const nsTArray<OwningFileOrDirectory>& a
{
ClearGetFilesHelpers();
- if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) {
+ if (IsWebkitFileSystemEnabled()) {
HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this);
mEntries.Clear();
}
@@ -3074,7 +3071,7 @@ HTMLInputElement::SetFiles(nsIDOMFileList* aFiles,
mFilesOrDirectories.Clear();
ClearGetFilesHelpers();
- if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) {
+ if (IsWebkitFileSystemEnabled()) {
HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this);
mEntries.Clear();
}
@@ -3097,14 +3094,14 @@ HTMLInputElement::MozSetDndFilesAndDirectories(const nsTArray<OwningFileOrDirect
{
SetFilesOrDirectories(aFilesOrDirectories, true);
- if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) {
+ if (IsWebkitFileSystemEnabled()) {
UpdateEntries(aFilesOrDirectories);
}
RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
new DispatchChangeEventCallback(this);
- if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ if (IsWebkitDirPickerEnabled() &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
ErrorResult rv;
GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */,
@@ -3182,8 +3179,8 @@ HTMLInputElement::GetFiles()
return nullptr;
}
- if (Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs() &&
- (!Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) ||
+ if (IsDirPickerEnabled() && Allowdirs() &&
+ (!IsWebkitDirPickerEnabled() ||
!HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
return nullptr;
}
@@ -4366,8 +4363,8 @@ HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor)
do_QueryInterface(aVisitor.mEvent->mOriginalTarget);
if (target &&
target->FindFirstNonChromeOnlyAccessContent() == this &&
- ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) ||
- (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+ ((IsDirPickerEnabled() && Allowdirs()) ||
+ (IsWebkitDirPickerEnabled() &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)))) {
type = FILE_PICKER_DIRECTORY;
}
@@ -5735,20 +5732,133 @@ HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult)
return true;
}
-static bool
-IsDateTimeEnabled(int32_t aNewType)
+/* static */ bool
+HTMLInputElement::IsDateTimeTypeSupported(uint8_t aDateTimeInputType)
+{
+ return (aDateTimeInputType == NS_FORM_INPUT_DATE &&
+ (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled() ||
+ IsDatePickerEnabled())) ||
+ (aDateTimeInputType == NS_FORM_INPUT_TIME &&
+ (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled())) ||
+ ((aDateTimeInputType == NS_FORM_INPUT_MONTH ||
+ aDateTimeInputType == NS_FORM_INPUT_WEEK ||
+ aDateTimeInputType == NS_FORM_INPUT_DATETIME_LOCAL) &&
+ IsInputDateTimeEnabled());
+}
+
+/* static */ bool
+HTMLInputElement::IsWebkitDirPickerEnabled()
+{
+ static bool sWebkitDirPickerEnabled = false;
+ static bool sWebkitDirPickerPrefCached = false;
+ if (!sWebkitDirPickerPrefCached) {
+ sWebkitDirPickerPrefCached = true;
+ Preferences::AddBoolVarCache(&sWebkitDirPickerEnabled,
+ "dom.webkitBlink.dirPicker.enabled",
+ false);
+ }
+
+ return sWebkitDirPickerEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsWebkitFileSystemEnabled()
+{
+ static bool sWebkitFileSystemEnabled = false;
+ static bool sWebkitFileSystemPrefCached = false;
+ if (!sWebkitFileSystemPrefCached) {
+ sWebkitFileSystemPrefCached = true;
+ Preferences::AddBoolVarCache(&sWebkitFileSystemEnabled,
+ "dom.webkitBlink.filesystem.enabled",
+ false);
+ }
+
+ return sWebkitFileSystemEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsDirPickerEnabled()
+{
+ static bool sDirPickerEnabled = false;
+ static bool sDirPickerPrefCached = false;
+ if (!sDirPickerPrefCached) {
+ sDirPickerPrefCached = true;
+ Preferences::AddBoolVarCache(&sDirPickerEnabled, "dom.input.dirpicker",
+ false);
+ }
+
+ return sDirPickerEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsDatePickerEnabled()
+{
+ static bool sDatePickerEnabled = false;
+ static bool sDatePickerPrefCached = false;
+ if (!sDatePickerPrefCached) {
+ sDatePickerPrefCached = true;
+ Preferences::AddBoolVarCache(&sDatePickerEnabled, "dom.forms.datepicker",
+ false);
+ }
+
+ return sDatePickerEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsExperimentalFormsEnabled()
{
- return (aNewType == NS_FORM_INPUT_DATE &&
- (Preferences::GetBool("dom.forms.datetime", false) ||
- Preferences::GetBool("dom.experimental_forms", false) ||
- Preferences::GetBool("dom.forms.datepicker", false))) ||
- (aNewType == NS_FORM_INPUT_TIME &&
- (Preferences::GetBool("dom.forms.datetime", false) ||
- Preferences::GetBool("dom.experimental_forms", false))) ||
- ((aNewType == NS_FORM_INPUT_MONTH ||
- aNewType == NS_FORM_INPUT_WEEK ||
- aNewType == NS_FORM_INPUT_DATETIME_LOCAL) &&
- Preferences::GetBool("dom.forms.datetime", false));
+ static bool sExperimentalFormsEnabled = false;
+ static bool sExperimentalFormsPrefCached = false;
+ if (!sExperimentalFormsPrefCached) {
+ sExperimentalFormsPrefCached = true;
+ Preferences::AddBoolVarCache(&sExperimentalFormsEnabled,
+ "dom.experimental_forms",
+ false);
+ }
+
+ return sExperimentalFormsEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsInputDateTimeEnabled()
+{
+ static bool sDateTimeEnabled = false;
+ static bool sDateTimePrefCached = false;
+ if (!sDateTimePrefCached) {
+ sDateTimePrefCached = true;
+ Preferences::AddBoolVarCache(&sDateTimeEnabled, "dom.forms.datetime",
+ false);
+ }
+
+ return sDateTimeEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsInputNumberEnabled()
+{
+ static bool sInputNumberEnabled = false;
+ static bool sInputNumberPrefCached = false;
+ if (!sInputNumberPrefCached) {
+ sInputNumberPrefCached = true;
+ Preferences::AddBoolVarCache(&sInputNumberEnabled, "dom.forms.number",
+ false);
+ }
+
+ return sInputNumberEnabled;
+}
+
+/* static */ bool
+HTMLInputElement::IsInputColorEnabled()
+{
+ static bool sInputColorEnabled = false;
+ static bool sInputColorPrefCached = false;
+ if (!sInputColorPrefCached) {
+ sInputColorPrefCached = true;
+ Preferences::AddBoolVarCache(&sInputColorEnabled, "dom.forms.color",
+ false);
+ }
+
+ return sInputColorEnabled;
}
bool
@@ -5766,12 +5876,11 @@ HTMLInputElement::ParseAttribute(int32_t aNamespaceID,
if (success) {
newType = aResult.GetEnumValue();
if ((IsExperimentalMobileType(newType) &&
- !Preferences::GetBool("dom.experimental_forms", false)) ||
- (newType == NS_FORM_INPUT_NUMBER &&
- !Preferences::GetBool("dom.forms.number", false)) ||
- (newType == NS_FORM_INPUT_COLOR &&
- !Preferences::GetBool("dom.forms.color", false)) ||
- (IsDateTimeInputType(newType) && !IsDateTimeEnabled(newType))) {
+ !IsExperimentalFormsEnabled()) ||
+ (newType == NS_FORM_INPUT_NUMBER && !IsInputNumberEnabled()) ||
+ (newType == NS_FORM_INPUT_COLOR && !IsInputColorEnabled()) ||
+ (IsDateTimeInputType(newType) &&
+ !IsDateTimeTypeSupported(newType))) {
newType = kInputDefaultType->value;
aResult.SetTo(newType, &aValue);
}
diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h
index 9ca876aee..9baaffb63 100644
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -1632,9 +1632,73 @@ private:
return IsSingleLineTextControl(false, aType) ||
aType == NS_FORM_INPUT_RANGE ||
aType == NS_FORM_INPUT_NUMBER ||
- aType == NS_FORM_INPUT_TIME;
+ aType == NS_FORM_INPUT_TIME ||
+ aType == NS_FORM_INPUT_DATE;
}
+ /**
+ * Checks if aDateTimeInputType should be supported based on "dom.forms.datetime",
+ * "dom.forms.datepicker" and "dom.experimental_forms".
+ */
+ static bool
+ IsDateTimeTypeSupported(uint8_t aDateTimeInputType);
+
+ /**
+ * Checks preference "dom.webkitBlink.dirPicker.enabled" to determine if
+ * webkitdirectory should be supported.
+ */
+ static bool
+ IsWebkitDirPickerEnabled();
+
+ /**
+ * Checks preference "dom.webkitBlink.filesystem.enabled" to determine if
+ * webkitEntries should be supported.
+ */
+ static bool
+ IsWebkitFileSystemEnabled();
+
+ /**
+ * Checks preference "dom.input.dirpicker" to determine if file and directory
+ * entries API should be supported.
+ */
+ static bool
+ IsDirPickerEnabled();
+
+ /**
+ * Checks preference "dom.forms.datepicker" to determine if date picker should
+ * be supported.
+ */
+ static bool
+ IsDatePickerEnabled();
+
+ /**
+ * Checks preference "dom.experimental_forms" to determine if experimental
+ * implementation of input element should be enabled.
+ */
+ static bool
+ IsExperimentalFormsEnabled();
+
+ /**
+ * Checks preference "dom.forms.datetime" to determine if input date/time
+ * related types should be supported.
+ */
+ static bool
+ IsInputDateTimeEnabled();
+
+ /**
+ * Checks preference "dom.forms.number" to determine if input type=number
+ * should be supported.
+ */
+ static bool
+ IsInputNumberEnabled();
+
+ /**
+ * Checks preference "dom.forms.color" to determine if date/time related
+ * types should be supported.
+ */
+ static bool
+ IsInputColorEnabled();
+
struct nsFilePickerFilter {
nsFilePickerFilter()
: mFilterMask(0) {}
diff --git a/dom/html/test/forms/mochitest.ini b/dom/html/test/forms/mochitest.ini
index 6fceefd98..199e4baf8 100644
--- a/dom/html/test/forms/mochitest.ini
+++ b/dom/html/test/forms/mochitest.ini
@@ -32,6 +32,8 @@ skip-if = android_version == '18' # Android, bug 1147974
skip-if = android_version == '18' # Android, bug 1147974
[test_input_date_key_events.html]
skip-if = os == "android"
+[test_input_datetime_input_change_events.html]
+skip-if = os == "android"
[test_input_datetime_focus_blur.html]
skip-if = os == "android"
[test_input_datetime_focus_blur_events.html]
diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html
index cd974e505..f502d6a4d 100644
--- a/dom/html/test/forms/test_input_date_key_events.html
+++ b/dom/html/test/forms/test_input_date_key_events.html
@@ -184,16 +184,16 @@ var testData = [
expectedVal: "2016-01-31"
},
{
- // Home key on year field sets it to the minimum year, which is 0001.
+ // Home key should have no effect on year field.
keys: ["VK_TAB", "VK_TAB", "VK_HOME"],
initialVal: "2016-01-01",
- expectedVal: "0001-01-01"
+ expectedVal: "2016-01-01"
},
{
- // End key on year field sets it to the maximum year, which is 275760.
+ // End key should have no effect on year field.
keys: ["VK_TAB", "VK_TAB", "VK_END"],
initialVal: "2016-01-01",
- expectedVal: "275760-01-01"
+ expectedVal: "2016-01-01"
},
];
diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html
new file mode 100644
index 000000000..e636995d3
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_input_change_events.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1370858
+-->
+<head>
+<title>Test for Bug 1370858</title>
+<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370858">Mozilla Bug 722599</a>
+<p id="display"></p>
+<div id="content">
+<input type="time" id="input_time" onchange="++changeEvents[0]"
+ oninput="++inputEvents[0]">
+<input type="date" id="input_date" onchange="++changeEvents[1]"
+ oninput="++inputEvents[1]">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/**
+ * Test for Bug 1370858.
+ * Test that change and input events are (not) fired for date/time inputs.
+ **/
+
+var inputTypes = ["time", "date"];
+var changeEvents = [0, 0];
+var inputEvents = [0, 0];
+var values = ["10:30", "2017-06-08"];
+var expectedValues = [["09:30", "01:30"], ["2017-05-08", "2017-01-08"]];
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ for (var i = 0; i < inputTypes.length; i++) {
+ var input = document.getElementById("input_" + inputTypes[i]);
+ var inputRect = input.getBoundingClientRect();
+
+ // Points over the input's reset button
+ var resetButton_X = inputRect.width - 15;
+ var resetButton_Y = inputRect.height / 2;
+
+ is(changeEvents[i], 0, "Number of change events should be 0 at start.");
+ is(inputEvents[i], 0, "Number of input events should be 0 at start.");
+
+ // Test that change and input events are not dispatched setting .value by
+ // script.
+ input.value = values[i];
+ is(input.value, values[i], "Check that value was set correctly (0).");
+ is(changeEvents[i], 0, "Change event should not have dispatched (0).");
+ is(inputEvents[i], 0, "Input event should not have dispatched (0).");
+
+ // Test that change and input events are fired when changing the value using
+ // up/down keys.
+ input.focus();
+ synthesizeKey("VK_DOWN", {});
+ is(input.value, expectedValues[i][0], "Check that value was set correctly (1).");
+ is(changeEvents[i], 1, "Change event should be dispatched (1).");
+ is(inputEvents[i], 1, "Input event should ne dispatched (1).");
+
+ // Test that change and input events are fired when changing the value with
+ // the keyboard.
+ synthesizeKey("0", {});
+ synthesizeKey("1", {});
+ is(input.value, expectedValues[i][1], "Check that value was set correctly (2).");
+ is(changeEvents[i], 2, "Change event should be dispatched (2).");
+ is(inputEvents[i], 2, "Input event should be dispatched (2).");
+
+ // Test that change and input events are fired when clearing the value using
+ // the reset button.
+ synthesizeMouse(input, resetButton_X, resetButton_Y, {});
+ is(input.value, "", "Check that value was set correctly (3).");
+ is(changeEvents[i], 3, "Change event should be dispatched (3).");
+ is(inputEvents[i], 3, "Input event should be dispatched (3).");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/reftest.list b/layout/reftests/forms/input/datetime/reftest.list
index 0ce2002bd..a62d56c7c 100644
--- a/layout/reftests/forms/input/datetime/reftest.list
+++ b/layout/reftests/forms/input/datetime/reftest.list
@@ -11,3 +11,14 @@ skip-if(!Android&&!B2G&&!Mulet) == time-simple-unthemed.html time-simple-untheme
# type change
skip-if(Android||B2G||Mulet) == to-time-from-other-type-unthemed.html time-simple-unthemed.html
skip-if(Android||B2G||Mulet) == from-time-to-other-type-unthemed.html from-time-to-other-type-unthemed-ref.html
+
+# content should not overflow on small width/height
+skip-if(Android) == time-small-width.html time-small-width-ref.html
+skip-if(Android) == time-small-height.html time-small-height-ref.html
+skip-if(Android) == time-small-width-height.html time-small-width-height-ref.html
+
+# content (text) should be left aligned
+skip-if(Android) == time-content-left-aligned.html time-content-left-aligned-ref.html
+
+# reset button should be right aligned
+skip-if(Android) fails-if(styloVsGecko) == time-reset-button-right-aligned.html time-reset-button-right-aligned-ref.html # bug 1372062
diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html
new file mode 100644
index 000000000..ad8be9adc
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input type="time" style="width: 200px;">
+ <!-- div to cover the right area -->
+ <div style="display:block; position:absolute; background-color:black;
+ top:0px; left:40px; width:200px; height:100px;"></div>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned.html b/layout/reftests/forms/input/datetime/time-content-left-aligned.html
new file mode 100644
index 000000000..aa910cddf
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-content-left-aligned.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input type="time" style="width: 50px;">
+ <!-- div to cover the right area -->
+ <div style="display:block; position:absolute; background-color:black;
+ top:0px; left:40px; width:200px; height:100px;"></div>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html
new file mode 100644
index 000000000..3d36f2068
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input type="time" value="10:00" style="float: right; color: white;">
+ <!-- div to cover the left area -->
+ <div style="display:block; position:absolute; background-color:black;
+ top:0px; right:30px; width:500px; height:100px;"></div>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html
new file mode 100644
index 000000000..72d5cc140
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input type="time" value="10:00" style="width: 150px; float: right;
+ color: white;">
+ <!-- div to cover the left area -->
+ <div style="display:block; position:absolute; background-color:black;
+ top:0px; right:30px; width:500px; height:100px;"></div>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-small-height-ref.html b/layout/reftests/forms/input/datetime/time-small-height-ref.html
new file mode 100644
index 000000000..fcda93df9
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-height-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+input {
+ width: 200px;
+ height: 5px;
+ outline: 1px dotted black;
+ /* Disable baseline alignment, so that our y-position isn't influenced by the
+ * choice of font inside of input: */
+ vertical-align: top;
+}
+ </style>
+ </head>
+ <body>
+ <input>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-small-height.html b/layout/reftests/forms/input/datetime/time-small-height.html
new file mode 100644
index 000000000..3044822fe
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-height.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+input {
+ width: 200px;
+ height: 5px;
+ outline: 1px dotted black;
+ color: white;
+ /* Disable baseline alignment, so that our y-position isn't influenced by the
+ * choice of font inside of input: */
+ vertical-align: top;
+}
+ </style>
+ </head>
+ <body>
+ <input type="time">
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-small-width-height-ref.html b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html
new file mode 100644
index 000000000..0979243db
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+input {
+ width: 8px;
+ height: 8px;
+ outline: 1px dotted black;
+ /* Disable baseline alignment, so that our y-position isn't influenced by the
+ * choice of font inside of input: */
+ vertical-align: top;
+}
+ </style>
+ </head>
+ <body>
+ <input>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-small-width-height.html b/layout/reftests/forms/input/datetime/time-small-width-height.html
new file mode 100644
index 000000000..a221b2819
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width-height.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+input {
+ width: 8px;
+ height: 8px;
+ outline: 1px dotted black;
+ color: white;
+ /* Disable baseline alignment, so that our y-position isn't influenced by the
+ * choice of font inside of input: */
+ vertical-align: top;
+}
+ </style>
+ </head>
+ <body>
+ <input type="time">
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-small-width-ref.html b/layout/reftests/forms/input/datetime/time-small-width-ref.html
new file mode 100644
index 000000000..2379c7080
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+input {
+ width: 10px;
+ height: 1.5em;
+ outline: 1px dotted black;
+ background: white;
+ /* Disable baseline alignment, so that our y-position isn't influenced by the
+ * choice of font inside of input: */
+ vertical-align: top;
+}
+ </style>
+ </head>
+ <body>
+ <input>
+ </body>
+</html>
diff --git a/layout/reftests/forms/input/datetime/time-small-width.html b/layout/reftests/forms/input/datetime/time-small-width.html
new file mode 100644
index 000000000..f76f7fdfa
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-small-width.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+input {
+ width: 10px;
+ height: 1.5em;
+ outline: 1px dotted black;
+ color: white;
+ background: white;
+ /* Disable baseline alignment, so that our y-position isn't influenced by the
+ * choice of font inside of input: */
+ vertical-align: top;
+}
+ </style>
+ </head>
+ <body>
+ <input type="time">
+ </body>
+</html>
diff --git a/layout/style/res/forms.css b/layout/style/res/forms.css
index f045540b1..e7566e183 100644
--- a/layout/style/res/forms.css
+++ b/layout/style/res/forms.css
@@ -1135,3 +1135,8 @@ input[type="number"] > div > div > div:hover {
/* give some indication of hover state for the up/down buttons */
background-color: lightblue;
}
+
+input[type="date"],
+input[type="time"] {
+ overflow: hidden !important;
+}
diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini
index 278b2ffe0..67ba2f850 100644
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -26,6 +26,7 @@ skip-if = !e10s
[browser_contentTitle.js]
[browser_crash_previous_frameloader.js]
run-if = e10s && crashreporter
+[browser_datetime_datepicker.js]
[browser_default_image_filename.js]
[browser_f7_caret_browsing.js]
[browser_findbar.js]
diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js
new file mode 100644
index 000000000..a0db761b7
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js
@@ -0,0 +1,222 @@
+/* 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/. */
+
+"use strict";
+
+const MONTH_YEAR = ".month-year",
+ DAYS_VIEW = ".days-view",
+ BTN_PREV_MONTH = ".prev",
+ BTN_NEXT_MONTH = ".next";
+const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format;
+
+// Create a list of abbreviations for calendar class names
+const W = "weekend",
+ O = "outside",
+ S = "selection",
+ R = "out-of-range",
+ T = "today",
+ P = "off-step";
+
+// Calendar classlist for 2016-12. Used to verify the classNames are correct.
+const calendarClasslist_201612 = [
+ [W, O], [O], [O], [O], [], [], [W],
+ [W], [], [], [], [], [], [W],
+ [W], [], [], [], [S], [], [W],
+ [W], [], [], [], [], [], [W],
+ [W], [], [], [], [], [], [W],
+ [W, O], [O], [O], [O], [O], [O], [W, O],
+];
+
+function getCalendarText() {
+ return helper.getChildren(DAYS_VIEW).map(child => child.textContent);
+}
+
+function getCalendarClassList() {
+ return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList));
+}
+
+function mergeArrays(a, b) {
+ return a.map((classlist, index) => classlist.concat(b[index]));
+}
+
+let helper = new DateTimeTestHelper();
+
+registerCleanupFunction(() => {
+ helper.cleanup();
+});
+
+/**
+ * Test that date picker opens to today's date when input field is blank
+ */
+add_task(async function test_datepicker_today() {
+ const date = new Date();
+
+ await helper.openPicker("data:text/html, <input type='date'>");
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date));
+
+ await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to the correct month, with calendar days
+ * displayed correctly, given a date value is set.
+ */
+add_task(async function test_datepicker_open() {
+ const inputValue = "2016-12-15";
+
+ await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue)));
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "27", "28", "29", "30", "1", "2", "3",
+ "4", "5", "6", "7", "8", "9", "10",
+ "11", "12", "13", "14", "15", "16", "17",
+ "18", "19", "20", "21", "22", "23", "24",
+ "25", "26", "27", "28", "29", "30", "31",
+ "1", "2", "3", "4", "5", "6", "7",
+ ],
+ "2016-12",
+ );
+ Assert.deepEqual(
+ getCalendarClassList(),
+ calendarClasslist_201612,
+ "2016-12 classNames"
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the prev month button is clicked, calendar should display the dates for
+ * the previous month.
+ */
+add_task(async function test_datepicker_prev_month_btn() {
+ const inputValue = "2016-12-15";
+ const prevMonth = "2016-11-01";
+
+ await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+ helper.click(helper.getElement(BTN_PREV_MONTH));
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth)));
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "30", "31", "1", "2", "3", "4", "5",
+ "6", "7", "8", "9", "10", "11", "12",
+ "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26",
+ "27", "28", "29", "30", "1", "2", "3",
+ "4", "5", "6", "7", "8", "9", "10",
+ ],
+ "2016-11",
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When the next month button is clicked, calendar should display the dates for
+ * the next month.
+ */
+add_task(async function test_datepicker_next_month_btn() {
+ const inputValue = "2016-12-15";
+ const nextMonth = "2017-01-01";
+
+ await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+ helper.click(helper.getElement(BTN_NEXT_MONTH));
+
+ Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth)));
+ Assert.deepEqual(
+ getCalendarText(),
+ [
+ "25", "26", "27", "28", "29", "30", "31",
+ "1", "2", "3", "4", "5", "6", "7",
+ "8", "9", "10", "11", "12", "13", "14",
+ "15", "16", "17", "18", "19", "20", "21",
+ "22", "23", "24", "25", "26", "27", "28",
+ "29", "30", "31", "1", "2", "3", "4",
+ ],
+ "2017-01",
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When a date on the calendar is clicked, date picker should close and set
+ * value to the input box.
+ */
+add_task(async function test_datepicker_clicked() {
+ const inputValue = "2016-12-15";
+ const firstDayOnCalendar = "2016-11-27";
+
+ await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+ // Click the first item (top-left corner) of the calendar
+ helper.click(helper.getElement(DAYS_VIEW).children[0]);
+ await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() {
+ let inputEl = content.document.querySelector("input");
+ await ContentTaskUtils.waitForEvent(inputEl, "input");
+ });
+
+ Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar);
+
+ await helper.tearDown();
+});
+
+/**
+ * When min and max attributes are set, calendar should show some dates as
+ * out-of-range.
+ */
+add_task(async function test_datepicker_min_max() {
+ const inputValue = "2016-12-15";
+ const inputMin = "2016-12-05";
+ const inputMax = "2016-12-25";
+
+ await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`);
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // R denotes out-of-range
+ [R], [R], [R], [R], [R], [R], [R],
+ [R], [], [], [], [], [], [],
+ [], [], [], [], [], [], [],
+ [], [], [], [], [], [], [],
+ [], [R], [R], [R], [R], [R], [R],
+ [R], [R], [R], [R], [R], [R], [R],
+ ]),
+ "2016-12 with min & max",
+ );
+
+ await helper.tearDown();
+});
+
+/**
+ * When step attribute is set, calendar should show some dates as off-step.
+ */
+add_task(async function test_datepicker_step() {
+ const inputValue = "2016-12-15";
+ const inputStep = "5";
+
+ await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">`);
+
+ Assert.deepEqual(
+ getCalendarClassList(),
+ mergeArrays(calendarClasslist_201612, [
+ // P denotes off-step
+ [P], [P], [P], [], [P], [P], [P],
+ [P], [], [P], [P], [P], [P], [],
+ [P], [P], [P], [P], [], [P], [P],
+ [P], [P], [], [P], [P], [P], [P],
+ [], [P], [P], [P], [P], [], [P],
+ [P], [P], [P], [], [P], [P], [P],
+ ]),
+ "2016-12 with step",
+ );
+
+ await helper.tearDown();
+});
diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js
index 1c6c2b54f..e3ef19538 100644
--- a/toolkit/content/tests/browser/head.js
+++ b/toolkit/content/tests/browser/head.js
@@ -31,3 +31,88 @@ function pushPrefs(...aPrefs) {
SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
return deferred.promise;
}
+
+/**
+ * Helper class for testing datetime input picker widget
+ */
+class DateTimeTestHelper {
+ constructor() {
+ this.panel = document.getElementById("DateTimePickerPanel");
+ this.panel.setAttribute("animate", false);
+ this.tab = null;
+ this.frame = null;
+ }
+
+ /**
+ * Opens a new tab with the URL of the test page, and make sure the picker is
+ * ready for testing.
+ *
+ * @param {String} pageUrl
+ */
+ async openPicker(pageUrl) {
+ this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+ await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser);
+ // If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached
+ if (!this.panel.dateTimePopupFrame) {
+ await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady")
+ }
+ this.frame = this.panel.dateTimePopupFrame;
+ await BrowserTestUtils.waitForEvent(this.frame, "load", true);
+ // Wait for picker elements to be ready and open panel transition to end
+ await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady");
+ }
+
+ /**
+ * Find an element on the picker.
+ *
+ * @param {String} selector
+ * @return {DOMElement}
+ */
+ getElement(selector) {
+ return this.frame.contentDocument.querySelector(selector);
+ }
+
+ /**
+ * Find the children of an element on the picker.
+ *
+ * @param {String} selector
+ * @return {Array<DOMElement>}
+ */
+ getChildren(selector) {
+ return Array.from(this.getElement(selector).children);
+ }
+
+ /**
+ * Click on an element
+ *
+ * @param {DOMElement} element
+ */
+ click(element) {
+ EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow);
+ }
+
+ /**
+ * Close the panel and the tab
+ */
+ async tearDown() {
+ if (!this.panel.hidden) {
+ let pickerClosePromise = new Promise(resolve => {
+ this.panel.addEventListener("popuphidden", resolve, {once: true});
+ });
+ this.panel.closePicker();
+ await pickerClosePromise;
+ }
+ await BrowserTestUtils.removeTab(this.tab);
+ this.tab = null;
+ }
+
+ /**
+ * Clean up after tests. Remove the frame to prevent leak.
+ */
+ cleanup() {
+ this.frame.remove();
+ this.frame = null;
+ this.panel.removeAttribute("animate");
+ this.panel = null;
+ }
+}
diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js
index 72e0d9d61..80c2976e0 100644
--- a/toolkit/content/widgets/calendar.js
+++ b/toolkit/content/widgets/calendar.js
@@ -54,23 +54,21 @@ function Calendar(options, context) {
* {
* {Number} textContent
* {Array<String>} classNames
+ * {Boolean} enabled
* }
* {Function} getDayString: Transform day number to string
* {Function} getWeekHeaderString: Transform day of week number to string
- * {Function} setValue: Set value for dateKeeper
- * {Number} selectionValue: The selection date value
+ * {Function} setSelection: Set selection for dateKeeper
* }
*/
setProps(props) {
if (props.isVisible) {
// Transform the days and weekHeaders array for rendering
- const days = props.days.map(({ dateValue, textContent, classNames }) => {
+ const days = props.days.map(({ dateObj, classNames, enabled }) => {
return {
- dateValue,
- textContent: props.getDayString(textContent),
- className: dateValue == props.selectionValue ?
- classNames.concat("selection").join(" ") :
- classNames.join(" ")
+ textContent: props.getDayString(dateObj.getUTCDate()),
+ className: classNames.join(" "),
+ enabled
};
});
const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => {
@@ -152,10 +150,10 @@ function Calendar(options, context) {
case "click": {
if (event.target.parentNode == this.context.daysView) {
let targetId = event.target.dataset.id;
- this.props.setValue({
- selectionValue: this.props.days[targetId].dateValue,
- dateValue: this.props.days[targetId].dateValue
- });
+ let targetObj = this.props.days[targetId];
+ if (targetObj.enabled) {
+ this.props.setSelection(targetObj.dateObj);
+ }
}
break;
}
diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js
index de01fdade..9777ee647 100644
--- a/toolkit/content/widgets/datekeeper.js
+++ b/toolkit/content/widgets/datekeeper.js
@@ -6,41 +6,72 @@
/**
* DateKeeper keeps track of the date states.
- *
- * @param {Object} date parts
- * {
- * {Number} year
- * {Number} month
- * {Number} day
- * }
- * {Object} options
- * {
- * {Number} firstDayOfWeek [optional]
- * {Array<Number>} weekends [optional]
- * {Number} calViewSize [optional]
- * }
*/
-function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
- this.state = {
- firstDayOfWeek, weekends, calViewSize,
- dateObj: new Date(0),
- years: [],
- months: [],
- days: []
- };
- this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek);
- this._update(year, month, day);
+function DateKeeper(props) {
+ this.init(props);
}
{
const DAYS_IN_A_WEEK = 7,
MONTHS_IN_A_YEAR = 12,
YEAR_VIEW_SIZE = 200,
- YEAR_BUFFER_SIZE = 10;
+ YEAR_BUFFER_SIZE = 10,
+ // The min and max values are derived from the ECMAScript spec:
+ // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
+ MIN_DATE = -8640000000000000,
+ MAX_DATE = 8640000000000000;
DateKeeper.prototype = {
+ get year() {
+ return this.state.dateObj.getUTCFullYear();
+ },
+
+ get month() {
+ return this.state.dateObj.getUTCMonth();
+ },
+
+ get day() {
+ return this.state.dateObj.getUTCDate();
+ },
+
+ get selection() {
+ return this.state.selection;
+ },
+
+ /**
+ * Initialize DateKeeper
+ * @param {Number} year
+ * @param {Number} month
+ * @param {Number} day
+ * @param {String} min
+ * @param {String} max
+ * @param {Number} firstDayOfWeek
+ * @param {Array<Number>} weekends
+ * @param {Number} calViewSize
+ */
+ init({ year, month, day, min, max, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
+ const today = new Date();
+ const isDateSet = year != undefined && month != undefined && day != undefined;
+
+ this.state = {
+ firstDayOfWeek, weekends, calViewSize,
+ min: new Date(min != undefined ? min : MIN_DATE),
+ max: new Date(max != undefined ? max : MAX_DATE),
+ today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()),
+ weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends),
+ years: [],
+ months: [],
+ days: [],
+ selection: { year, month, day },
+ };
+
+ this.state.dateObj = isDateSet ?
+ this._newUTCDate(year, month, day) :
+ new Date(this.state.today);
+ },
/**
- * Set new date
+ * Set new date. The year is always treated as full year, so the short-form
+ * is not supported.
* @param {Object} date parts
* {
* {Number} year [optional]
@@ -48,17 +79,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* {Number} date [optional]
* }
*/
- set({ year = this.state.year, month = this.state.month, day = this.state.day }) {
- this._update(year, month, day);
+ set({ year = this.year, month = this.month, day = this.day }) {
+ // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
+ this.state.dateObj.setUTCFullYear(year, month, day);
},
/**
- * Set date with value
- * @param {Number} value: Date value
+ * Set selection date
+ * @param {Number} year
+ * @param {Number} month
+ * @param {Number} day
*/
- setValue(value) {
- const dateObj = new Date(value);
- this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
+ setSelection({ year, month, day }) {
+ this.state.selection.year = year;
+ this.state.selection.month = month;
+ this.state.selection.day = day;
},
/**
@@ -66,8 +101,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* @param {Number} month
*/
setMonth(month) {
- const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate();
- this._update(this.state.year, month, Math.min(this.state.day, lastDayOfMonth));
+ const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate();
+ this.set({ year: this.year,
+ month,
+ day: Math.min(this.day, lastDayOfMonth) });
},
/**
@@ -75,8 +112,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* @param {Number} year
*/
setYear(year) {
- const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate();
- this._update(year, this.state.month, Math.min(this.state.day, lastDayOfMonth));
+ const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate();
+ this.set({ year,
+ month: this.month,
+ day: Math.min(this.day, lastDayOfMonth) });
},
/**
@@ -84,22 +123,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* @param {Number} offset
*/
setMonthByOffset(offset) {
- const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate();
- this._update(this.state.year, this.state.month + offset, Math.min(this.state.day, lastDayOfMonth));
- },
-
- /**
- * Update the states.
- * @param {Number} year [description]
- * @param {Number} month [description]
- * @param {Number} day [description]
- */
- _update(year, month, day) {
- // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
- this.state.dateObj.setUTCFullYear(year, month, day);
- this.state.year = this.state.dateObj.getUTCFullYear();
- this.state.month = this.state.dateObj.getUTCMonth();
- this.state.day = this.state.dateObj.getUTCDate();
+ const lastDayOfMonth = this._newUTCDate(this.year, this.month + offset + 1, 0).getUTCDate();
+ this.set({ year: this.year,
+ month: this.month + offset,
+ day: Math.min(this.day, lastDayOfMonth) });
},
/**
@@ -111,7 +138,6 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* }
*/
getMonths() {
- // TODO: add min/max and step support
let months = [];
for (let i = 0; i < MONTHS_IN_A_YEAR; i++) {
@@ -133,12 +159,11 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* }
*/
getYears() {
- // TODO: add min/max and step support
let years = [];
const firstItem = this.state.years[0];
const lastItem = this.state.years[this.state.years.length - 1];
- const currentYear = this.state.dateObj.getUTCFullYear();
+ const currentYear = this.year;
// Generate new years array when the year is outside of the first &
// last item range. If not, return the cached result.
@@ -161,30 +186,43 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
* Get days for calendar
* @return {Array<Object>}
* {
- * {Number} dateValue
- * {Number} textContent
+ * {Date} dateObj
* {Array<String>} classNames
+ * {Boolean} enabled
* }
*/
getDays() {
- // TODO: add min/max and step support
- let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
+ // TODO: add step support
+ const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
+ const month = this.month;
let days = [];
- let month = this.state.dateObj.getUTCMonth();
for (let i = 0; i < this.state.calViewSize; i++) {
- let dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i);
+ const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i);
let classNames = [];
+ let enabled = true;
if (this.state.weekends.includes(dateObj.getUTCDay())) {
classNames.push("weekend");
}
if (month != dateObj.getUTCMonth()) {
classNames.push("outside");
}
+ if (this.state.selection.year == dateObj.getUTCFullYear() &&
+ this.state.selection.month == dateObj.getUTCMonth() &&
+ this.state.selection.day == dateObj.getUTCDate()) {
+ classNames.push("selection");
+ }
+ if (dateObj.getTime() < this.state.min.getTime() || dateObj.getTime() > this.state.max.getTime()) {
+ classNames.push("out-of-range");
+ enabled = false;
+ }
+ if (this.state.today.getTime() == dateObj.getTime()) {
+ classNames.push("today");
+ }
days.push({
- dateValue: dateObj.getTime(),
- textContent: dateObj.getUTCDate(),
- classNames
+ dateObj,
+ classNames,
+ enabled,
});
}
return days;
@@ -193,20 +231,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
/**
* Get week headers for calendar
* @param {Number} firstDayOfWeek
+ * @param {Array<Number>} weekends
* @return {Array<Object>}
* {
* {Number} textContent
* {Array<String>} classNames
* }
*/
- _getWeekHeaders(firstDayOfWeek) {
+ _getWeekHeaders(firstDayOfWeek, weekends) {
let headers = [];
let dayOfWeek = firstDayOfWeek;
for (let i = 0; i < DAYS_IN_A_WEEK; i++) {
headers.push({
textContent: dayOfWeek % DAYS_IN_A_WEEK,
- classNames: this.state.weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : []
+ classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : []
});
dayOfWeek++;
}
diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js
index 210ca856c..317f0ae94 100644
--- a/toolkit/content/widgets/datepicker.js
+++ b/toolkit/content/widgets/datepicker.js
@@ -20,6 +20,12 @@ function DatePicker(context) {
* {Number} year [optional]
* {Number} month [optional]
* {Number} date [optional]
+ * {String} min
+ * {String} max
+ * {Number} firstDayOfWeek
+ * {Array<Number>} weekends
+ * {Array<String>} monthStrings
+ * {Array<String>} weekdayStrings
* {String} locale [optional]: User preferred locale
* }
*/
@@ -28,57 +34,54 @@ function DatePicker(context) {
this._setDefaultState();
this._createComponents();
this._update();
+ document.dispatchEvent(new CustomEvent("PickerReady"));
},
/*
* Set initial date picker states.
*/
_setDefaultState() {
- const now = new Date();
- const { year = now.getFullYear(),
- month = now.getMonth(),
- day = now.getDate(),
- locale } = this.props;
-
- // TODO: Use calendar info API to get first day of week & weekends
- // (Bug 1287503)
+ const { year, month, day, min, max, firstDayOfWeek, weekends,
+ monthStrings, weekdayStrings, locale } = this.props;
const dateKeeper = new DateKeeper({
- year, month, day
- }, {
- calViewSize: CAL_VIEW_SIZE,
- firstDayOfWeek: 0,
- weekends: [0]
+ year, month, day, min, max, firstDayOfWeek, weekends,
+ calViewSize: CAL_VIEW_SIZE
});
this.state = {
dateKeeper,
locale,
isMonthPickerVisible: false,
- isYearSet: false,
- isMonthSet: false,
- isDateSet: false,
getDayString: new Intl.NumberFormat(locale).format,
- // TODO: use calendar terms when available (Bug 1287677)
- getWeekHeaderString: weekday => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday],
- setValue: ({ dateValue, selectionValue }) => {
- dateKeeper.setValue(dateValue);
- this.state.selectionValue = selectionValue;
- this.state.isYearSet = true;
- this.state.isMonthSet = true;
- this.state.isDateSet = true;
+ getWeekHeaderString: weekday => weekdayStrings[weekday],
+ getMonthString: month => monthStrings[month],
+ setSelection: date => {
+ dateKeeper.setSelection({
+ year: date.getUTCFullYear(),
+ month: date.getUTCMonth(),
+ day: date.getUTCDate(),
+ });
this._update();
this._dispatchState();
this._closePopup();
},
setYear: year => {
dateKeeper.setYear(year);
- this.state.isYearSet = true;
+ dateKeeper.setSelection({
+ year,
+ month: dateKeeper.selection.month,
+ day: dateKeeper.selection.day,
+ });
this._update();
this._dispatchState();
},
setMonth: month => {
dateKeeper.setMonth(month);
- this.state.isMonthSet = true;
+ dateKeeper.setSelection({
+ year: dateKeeper.selection.year,
+ month,
+ day: dateKeeper.selection.day,
+ });
this._update();
this._dispatchState();
},
@@ -104,6 +107,7 @@ function DatePicker(context) {
monthYear: new MonthYear({
setYear: this.state.setYear,
setMonth: this.state.setMonth,
+ getMonthString: this.state.getMonthString,
locale: this.state.locale
}, {
monthYear: this.context.monthYear,
@@ -116,7 +120,7 @@ function DatePicker(context) {
* Update date picker and its components.
*/
_update() {
- const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state;
+ const { dateKeeper, isMonthPickerVisible } = this.state;
if (isMonthPickerVisible) {
this.state.months = dateKeeper.getMonths();
@@ -128,9 +132,7 @@ function DatePicker(context) {
this.components.monthYear.setProps({
isVisible: isMonthPickerVisible,
dateObj: dateKeeper.state.dateObj,
- month: dateKeeper.state.month,
months: this.state.months,
- year: dateKeeper.state.year,
years: this.state.years,
toggleMonthPicker: this.state.toggleMonthPicker
});
@@ -138,10 +140,9 @@ function DatePicker(context) {
isVisible: !isMonthPickerVisible,
days: this.state.days,
weekHeaders: dateKeeper.state.weekHeaders,
- setValue: this.state.setValue,
+ setSelection: this.state.setSelection,
getDayString: this.state.getDayString,
- getWeekHeaderString: this.state.getWeekHeaderString,
- selectionValue
+ getWeekHeaderString: this.state.getWeekHeaderString
});
isMonthPickerVisible ?
@@ -162,8 +163,7 @@ function DatePicker(context) {
* Use postMessage to pass the state of picker to the panel.
*/
_dispatchState() {
- const { year, month, day } = this.state.dateKeeper.state;
- const { isYearSet, isMonthSet, isDaySet } = this.state;
+ const { year, month, day } = this.state.dateKeeper.selection;
// The panel is listening to window for postMessage event, so we
// do postMessage to itself to send data to input boxes.
window.postMessage({
@@ -172,9 +172,6 @@ function DatePicker(context) {
year,
month,
day,
- isYearSet,
- isMonthSet,
- isDaySet
}
}, "*");
},
@@ -255,19 +252,12 @@ function DatePicker(context) {
set({ year, month, day }) {
const { dateKeeper } = this.state;
- if (year != undefined) {
- this.state.isYearSet = true;
- }
- if (month != undefined) {
- this.state.isMonthSet = true;
- }
- if (day != undefined) {
- this.state.isDaySet = true;
- }
-
dateKeeper.set({
year, month, day
});
+ dateKeeper.setSelection({
+ year, month, day
+ });
this._update();
}
};
@@ -280,12 +270,12 @@ function DatePicker(context) {
* {String} locale
* {Function} setYear
* {Function} setMonth
+ * {Function} getMonthString
* }
* @param {DOMElement} context
*/
function MonthYear(options, context) {
const spinnerSize = 5;
- const monthFormat = new Intl.DateTimeFormat(options.locale, { month: "short" }).format;
const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric" }).format;
const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long" }).format;
@@ -298,7 +288,7 @@ function DatePicker(context) {
this.state.isMonthSet = true;
options.setMonth(month);
},
- getDisplayString: month => monthFormat(new Date(0, month)),
+ getDisplayString: options.getMonthString,
viewportSize: spinnerSize
}, context.monthYearView),
year: new Spinner({
@@ -323,8 +313,6 @@ function DatePicker(context) {
* {
* {Boolean} isVisible
* {Date} dateObj
- * {Number} month
- * {Number} year
* {Array<Object>} months
* {Array<Object>} years
* {Function} toggleMonthPicker
@@ -336,14 +324,14 @@ function DatePicker(context) {
if (props.isVisible) {
this.context.monthYear.classList.add("active");
this.components.month.setState({
- value: props.month,
+ value: props.dateObj.getUTCMonth(),
items: props.months,
isInfiniteScroll: true,
isValueSet: this.state.isMonthSet,
smoothScroll: !this.state.firstOpened
});
this.components.year.setState({
- value: props.year,
+ value: props.dateObj.getUTCFullYear(),
items: props.years,
isInfiniteScroll: false,
isValueSet: this.state.isYearSet,
diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css
index 18ff024c7..ce638078f 100644
--- a/toolkit/content/widgets/datetimebox.css
+++ b/toolkit/content/widgets/datetimebox.css
@@ -8,9 +8,17 @@
.datetime-input-box-wrapper {
-moz-appearance: none;
display: inline-flex;
+ flex: 1;
cursor: default;
background-color: inherit;
color: inherit;
+ min-width: 0;
+ justify-content: space-between;
+}
+
+.datetime-input-edit-wrapper {
+ overflow: hidden;
+ white-space: nowrap;
}
.datetime-input {
@@ -43,5 +51,5 @@
height: 12px;
width: 12px;
align-self: center;
- justify-content: flex-end;
+ flex: none;
}
diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml
index 5859f80dd..c276265a3 100644
--- a/toolkit/content/widgets/datetimebox.xml
+++ b/toolkit/content/widgets/datetimebox.xml
@@ -4,6 +4,11 @@
- 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/. -->
+<!DOCTYPE bindings [
+<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd">
+%datetimeboxDTD;
+]>
+
<bindings id="datetimeboxBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
@@ -21,12 +26,13 @@
<implementation>
<constructor>
<![CDATA[
- // TODO: Bug 1320227 - [DateTimeInput] localization for
- // <input type=date> input box
- this.mMonthPlaceHolder = "mm";
- this.mDayPlaceHolder = "dd";
- this.mYearPlaceHolder = "yyyy";
+ /* eslint-disable no-multi-spaces */
+ this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[;
+ this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[;
+ this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[;
this.mSeparatorText = "/";
+ /* eslint-enable no-multi-spaces */
+
this.mMinMonth = 1;
this.mMaxMonth = 12;
this.mMinDay = 1;
@@ -76,6 +82,7 @@
if (this.mInputElement.value) {
this.setFieldsFromInputValue();
}
+ this.updateResetButtonVisibility();
]]>
</constructor>
@@ -107,9 +114,11 @@
this.mYearField.setAttribute("typeBuffer", "");
}
- if (!aFromInputElement) {
+ if (!aFromInputElement && this.mInputElement.value) {
this.mInputElement.setUserInput("");
}
+
+ this.updateResetButtonVisibility();
]]>
</body>
</method>
@@ -170,6 +179,13 @@
<method name="setInputValueFromFields">
<body>
<![CDATA[
+ if (!this.isAnyValueAvailable(false) && this.mInputElement.value) {
+ // Values in the input box was cleared, clear the input element's
+ // value if not empty.
+ this.mInputElement.setUserInput("");
+ return;
+ }
+
if (this.isFieldInvalid(this.mYearField) ||
this.isFieldInvalid(this.mMonthField) ||
this.isFieldInvalid(this.mDayField)) {
@@ -192,6 +208,10 @@
let date = [year, month, day].join("-");
+ if (date == this.mInputElement.value) {
+ return;
+ }
+
this.log("setInputValueFromFields: " + date);
this.mInputElement.setUserInput(date);
]]>
@@ -217,6 +237,9 @@
if (!this.isEmpty(day)) {
this.setFieldValue(this.mDayField, day);
}
+
+ // Update input element's .value if needed.
+ this.setInputValueFromFields();
]]>
</body>
</method>
@@ -302,6 +325,12 @@
let targetField = aEvent.originalTarget;
let key = aEvent.key;
+ // Home/End key does nothing on year field.
+ if (targetField == this.mYearField && (key == "Home" ||
+ key == "End")) {
+ return;
+ }
+
switch (key) {
case "ArrowUp":
this.incrementFieldValue(targetField, 1);
@@ -406,11 +435,13 @@
}
aField.value = value;
+ this.updateResetButtonVisibility();
]]>
</body>
</method>
- <method name="isValueAvailable">
+ <method name="isAnyValueAvailable">
+ <parameter name="aForPicker"/>
<body>
<![CDATA[
return !this.isEmpty(this.mMonthField.value) ||
@@ -492,6 +523,7 @@
if (this.mInputElement.value) {
this.setFieldsFromInputValue();
}
+ this.updateResetButtonVisibility();
]]>
</constructor>
@@ -551,7 +583,7 @@
}
this.log("setFieldsFromInputValue: " + value);
- let [hour, minute, second] = value.split(':');
+ let [hour, minute, second] = value.split(":");
this.setFieldValue(this.mHourField, hour);
this.setFieldValue(this.mMinuteField, minute);
@@ -617,6 +649,13 @@
<method name="setInputValueFromFields">
<body>
<![CDATA[
+ if (!this.isAnyValueAvailable(false) && this.mInputElement.value) {
+ // Values in the input box was cleared, clear the input element's
+ // value if not empty.
+ this.mInputElement.setUserInput("");
+ return;
+ }
+
if (this.isEmpty(this.mHourField.value) ||
this.isEmpty(this.mMinuteField.value) ||
(this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) ||
@@ -652,6 +691,10 @@
time += "." + this.mMillisecField.value;
}
+ if (time == this.mInputElement.value) {
+ return;
+ }
+
this.log("setInputValueFromFields: " + time);
this.mInputElement.setUserInput(time);
]]>
@@ -678,6 +721,9 @@
if (!this.isEmpty(minute)) {
this.setFieldValue(this.mMinuteField, minute);
}
+
+ // Update input element's .value if needed.
+ this.setInputValueFromFields();
]]>
</body>
</method>
@@ -721,9 +767,11 @@
this.mDayPeriodField.value = "";
}
- if (!aFromInputElement) {
+ if (!aFromInputElement && this.mInputElement.value) {
this.mInputElement.setUserInput("");
}
+
+ this.updateResetButtonVisibility();
]]>
</body>
</method>
@@ -793,6 +841,7 @@
this.mDayPeriodField.value == this.mAMIndicator ?
this.mPMIndicator : this.mAMIndicator;
this.mDayPeriodField.select();
+ this.updateResetButtonVisibility();
this.setInputValueFromFields();
return;
}
@@ -850,6 +899,7 @@
this.mDayPeriodField.value = this.mPMIndicator;
this.mDayPeriodField.select();
}
+ this.updateResetButtonVisibility();
return;
}
@@ -905,16 +955,30 @@
}
aField.value = value;
+ this.updateResetButtonVisibility();
]]>
</body>
</method>
- <method name="isValueAvailable">
+ <method name="isAnyValueAvailable">
+ <parameter name="aForPicker"/>
<body>
<![CDATA[
+ let available = !this.isEmpty(this.mHourField.value) ||
+ !this.isEmpty(this.mMinuteField.value);
+
+ if (available) {
+ return true;
+ }
+
// Picker only cares about hour:minute.
- return !this.isEmpty(this.mHourField.value) ||
- !this.isEmpty(this.mMinuteField.value);
+ 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));
]]>
</body>
</method>
@@ -963,7 +1027,8 @@
<content>
<html:div class="datetime-input-box-wrapper"
xbl:inherits="context,disabled,readonly">
- <html:span>
+ <html:span class="datetime-input-edit-wrapper"
+ anonid="edit-wrapper">
<html:input anonid="input-one"
class="textbox-input datetime-input numeric"
size="2" maxlength="2"
@@ -980,9 +1045,8 @@
xbl:inherits="disabled,readonly,tabindex"/>
</html:span>
- <html:button class="datetime-reset-button" anoid="reset-button"
- tabindex="-1" xbl:inherits="disabled"
- onclick="document.getBindingParent(this).clearInputFields(false);"/>
+ <html:button class="datetime-reset-button" anonid="reset-button"
+ tabindex="-1" xbl:inherits="disabled"/>
</html:div>
</content>
@@ -997,6 +1061,9 @@
this.mStep = this.mInputElement.step;
this.mIsPickerOpen = false;
+ this.mResetButton =
+ document.getAnonymousElementByAttribute(this, "anonid", "reset-button");
+
this.EVENTS.forEach((eventName) => {
this.addEventListener(eventName, this, { mozSystemGroup: true });
});
@@ -1005,6 +1072,9 @@
capture: true,
mozSystemGroup: true
});
+ // This is to close the picker when input element blurs.
+ this.mInputElement.addEventListener("blur", this,
+ { mozSystemGroup: true });
]]>
</constructor>
@@ -1025,7 +1095,7 @@
<property name="EVENTS" readonly="true">
<getter>
<![CDATA[
- return ["click", "focus", "blur", "copy", "cut", "paste"];
+ return ["click", "focus", "blur", "copy", "cut", "paste", "mousedown"];
]]>
</getter>
</property>
@@ -1041,6 +1111,18 @@
</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[
@@ -1164,10 +1246,16 @@
</body>
</method>
+ <method name="isAnyValueAvailable">
+ <body>
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ </body>
+ </method>
+
<method name="notifyPicker">
<body>
<![CDATA[
- if (this.mIsPickerOpen && this.isValueAvailable()) {
+ if (this.mIsPickerOpen && this.isAnyValueAvailable(true)) {
this.mInputElement.updateDateTimePicker(this.getCurrentValue());
}
]]>
@@ -1213,6 +1301,12 @@
this.onBlur(aEvent);
break;
}
+ case "mousedown": {
+ if (aEvent.originalTarget == this.mResetButton) {
+ aEvent.preventDefault();
+ }
+ break;
+ }
case "copy":
case "cut":
case "paste": {
@@ -1245,7 +1339,12 @@
<parameter name="aEvent"/>
<body>
<![CDATA[
- this.log("onBlur originalTarget: " + aEvent.originalTarget);
+ this.log("onBlur originalTarget: " + aEvent.originalTarget +
+ " target: " + aEvent.target);
+
+ if (aEvent.target == this.mInputElement && this.mIsPickerOpen) {
+ this.mInputElement.closeDateTimePicker();
+ }
let target = aEvent.originalTarget;
target.setAttribute("typeBuffer", "");
@@ -1261,16 +1360,23 @@
this.log("onKeyPress key: " + aEvent.key);
switch (aEvent.key) {
- // Close picker on Enter or Space key.
+ // Close picker on Enter, Escape or Space key.
case "Enter":
+ case "Escape":
case " ": {
- this.mInputElement.closeDateTimePicker();
- aEvent.preventDefault();
+ if (this.mIsPickerOpen) {
+ this.mInputElement.closeDateTimePicker();
+ aEvent.preventDefault();
+ }
break;
}
case "Backspace": {
let targetField = aEvent.originalTarget;
+ targetField.value = "";
targetField.setAttribute("typeBuffer", "");
+ this.updateResetButtonVisibility();
+ this.setInputValueFromFields();
+ aEvent.preventDefault();
break;
}
case "ArrowRight":
@@ -1312,12 +1418,14 @@
// XXX: .originalTarget is not expected.
// When clicking on one of the inner text boxes, the .originalTarget is
// a HTMLDivElement and when clicking on the reset button, it's a
- // HTMLButtonElement but it's not equal to our reset-button.
+ // HTMLButtonElement.
if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) {
return;
}
- if (!(aEvent.originalTarget instanceof HTMLButtonElement)) {
+ if (aEvent.originalTarget == this.mResetButton) {
+ this.clearInputFields(false);
+ } else if (!this.mIsPickerOpen) {
this.mInputElement.openDateTimePicker(this.getCurrentValue());
}
]]>
diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml
index 5f16f1ff0..1d6a5e772 100644
--- a/toolkit/content/widgets/datetimepicker.xml
+++ b/toolkit/content/widgets/datetimepicker.xml
@@ -999,13 +999,13 @@
<body>
<![CDATA[
var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory";
- var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"});
+ var dtfMonth = Intl.DateTimeFormat(locale, {month: "long", timeZone: "UTC"});
var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"});
var monthLabel = this.monthField.firstChild;
- var tempDate = new Date(2005, 0, 1);
+ var tempDate = new Date(Date.UTC(2005, 0, 1));
for (var month = 0; month < 12; month++) {
- tempDate.setMonth(month);
+ tempDate.setUTCMonth(month);
monthLabel.setAttribute("value", dtfMonth.format(tempDate));
monthLabel = monthLabel.nextSibling;
}
diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml
index 86e8780c1..52df7de75 100644
--- a/toolkit/content/widgets/datetimepopup.xml
+++ b/toolkit/content/widgets/datetimepopup.xml
@@ -22,11 +22,20 @@
<field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field>
<field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field>
<field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field>
- <method name="loadPicker">
+ <constructor><![CDATA[
+ this.l10n = {};
+ const mozIntl = Components.classes["@mozilla.org/mozintl;1"]
+ .getService(Components.interfaces.mozIMozIntl);
+ mozIntl.addGetCalendarInfo(l10n);
+ mozIntl.addGetDisplayNames(l10n);
+ // Notify DateTimePickerHelper.jsm that binding is ready.
+ this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady"));
+ ]]></constructor>
+ <method name="openPicker">
<parameter name="type"/>
+ <parameter name="anchor"/>
<parameter name="detail"/>
<body><![CDATA[
- this.hidden = false;
this.type = type;
this.pickerState = {};
// TODO: Resize picker according to content zoom level
@@ -49,17 +58,20 @@
break;
}
}
+ this.hidden = false;
+ this.openPopup(anchor, "after_start", 0, 0);
]]></body>
</method>
<method name="closePicker">
<body><![CDATA[
- this.hidden = true;
this.setInputBoxValue(true);
this.pickerState = {};
this.type = undefined;
this.dateTimePopupFrame.removeEventListener("load", this, true);
this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false);
this.dateTimePopupFrame.setAttribute("src", "");
+ this.hidePopup();
+ this.hidden = true;
]]></body>
</method>
<method name="setPopupValue">
@@ -115,6 +127,34 @@
}
case "date": {
const { year, month, day } = detail.value;
+ const { firstDayOfWeek, weekends } =
+ this.getCalendarInfo(locale);
+ const monthStrings = this.getDisplayNames(
+ locale, [
+ "dates/gregorian/months/january",
+ "dates/gregorian/months/february",
+ "dates/gregorian/months/march",
+ "dates/gregorian/months/april",
+ "dates/gregorian/months/may",
+ "dates/gregorian/months/june",
+ "dates/gregorian/months/july",
+ "dates/gregorian/months/august",
+ "dates/gregorian/months/september",
+ "dates/gregorian/months/october",
+ "dates/gregorian/months/november",
+ "dates/gregorian/months/december",
+ ], "short");
+ const weekdayStrings = this.getDisplayNames(
+ locale, [
+ "dates/gregorian/weekdays/sunday",
+ "dates/gregorian/weekdays/monday",
+ "dates/gregorian/weekdays/tuesday",
+ "dates/gregorian/weekdays/wednesday",
+ "dates/gregorian/weekdays/thursday",
+ "dates/gregorian/weekdays/friday",
+ "dates/gregorian/weekdays/saturday",
+ ], "short");
+
this.postMessageToPicker({
name: "PickerInit",
detail: {
@@ -122,7 +162,13 @@
// Month value from input box starts from 1 instead of 0
month: month == undefined ? undefined : month - 1,
day,
- locale
+ firstDayOfWeek,
+ weekends,
+ monthStrings,
+ weekdayStrings,
+ locale,
+ min: detail.min,
+ max: detail.max,
}
});
break;
@@ -184,6 +230,46 @@
}
]]></body>
</method>
+ <method name="getCalendarInfo">
+ <parameter name="locale"/>
+ <body><![CDATA[
+ const calendarInfo = this.l10n.getCalendarInfo(locale);
+
+ // Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday,
+ // so they need to be mapped to JavaScript convention with 0 as Sunday
+ // and 6 as Saturday
+ let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1,
+ weekendStart = calendarInfo.weekendStart - 1,
+ weekendEnd = calendarInfo.weekendEnd - 1;
+
+ let weekends = [];
+
+ // Make sure weekendEnd is greater than weekendStart
+ if (weekendEnd < weekendStart) {
+ weekendEnd += 7;
+ }
+
+ // We get the weekends by incrementing weekendStart up to weekendEnd.
+ // If the start and end is the same day, then weekends only has one day.
+ for (let day = weekendStart; day <= weekendEnd; day++) {
+ weekends.push(day % 7);
+ }
+
+ return {
+ firstDayOfWeek,
+ weekends
+ }
+ ]]></body>
+ </method>
+ <method name="getDisplayNames">
+ <parameter name="locale"/>
+ <parameter name="keys"/>
+ <parameter name="style"/>
+ <body><![CDATA[
+ const displayNames = this.l10n.getDisplayNames(locale, {keys, style});
+ return keys.map(key => displayNames.values[key]);
+ ]]></body>
+ </method>
<method name="handleEvent">
<parameter name="aEvent"/>
<body><![CDATA[
@@ -230,12 +316,5 @@
</method>
</implementation>
- <handlers>
- <handler event="popuphiding">
- <![CDATA[
- this.closePicker();
- ]]>
- </handler>
- </handlers>
</binding>
</bindings>
diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js
index 059e151fc..6ef929f8a 100644
--- a/toolkit/content/widgets/spinner.js
+++ b/toolkit/content/widgets/spinner.js
@@ -299,11 +299,11 @@ function Spinner(props, context) {
// An "active" class is needed to simulate :active pseudo-class
// because element is not focused.
event.target.classList.add("active");
- this._smoothScrollToIndex(index + 1);
+ this._smoothScrollToIndex(index - 1);
}
if (event.target == down) {
event.target.classList.add("active");
- this._smoothScrollToIndex(index - 1);
+ this._smoothScrollToIndex(index + 1);
}
if (event.target.parentNode == spinner) {
// Listen to dragging events
diff --git a/toolkit/locales/en-US/chrome/global/datetimebox.dtd b/toolkit/locales/en-US/chrome/global/datetimebox.dtd
new file mode 100644
index 000000000..0deffa6b3
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/datetimebox.dtd
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+
+<!-- Placeholders for input type=date -->
+
+<!ENTITY date.year.placeholder "yyyy">
+<!ENTITY date.month.placeholder "mm">
+<!ENTITY date.day.placeholder "dd">
diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn
index e49e978f5..abc96086f 100644
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -39,6 +39,7 @@
locale/@AB_CD@/global/customizeToolbar.dtd (%chrome/global/customizeToolbar.dtd)
locale/@AB_CD@/global/customizeToolbar.properties (%chrome/global/customizeToolbar.properties)
#endif
+ locale/@AB_CD@/global/datetimebox.dtd (%chrome/global/datetimebox.dtd)
locale/@AB_CD@/global/datetimepicker.dtd (%chrome/global/datetimepicker.dtd)
locale/@AB_CD@/global/dateFormat.properties (%chrome/global/dateFormat.properties)
locale/@AB_CD@/global/dialogOverlay.dtd (%chrome/global/dialogOverlay.dtd)
diff --git a/toolkit/modules/DateTimePickerHelper.jsm b/toolkit/modules/DateTimePickerHelper.jsm
index 769ae0094..0ea96f226 100644
--- a/toolkit/modules/DateTimePickerHelper.jsm
+++ b/toolkit/modules/DateTimePickerHelper.jsm
@@ -21,6 +21,7 @@ this.EXPORTED_SYMBOLS = [
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
/*
* DateTimePickerHelper receives message from content side (input box) and
@@ -66,6 +67,9 @@ this.DateTimePickerHelper = {
break;
}
case "FormDateTime:UpdatePicker": {
+ if (!this.picker) {
+ return;
+ }
this.picker.setPopupValue(aMessage.data);
break;
}
@@ -105,7 +109,7 @@ this.DateTimePickerHelper = {
},
// Get picker from browser and show it anchored to the input box.
- showPicker: function(aBrowser, aData) {
+ showPicker: Task.async(function* (aBrowser, aData) {
let rect = aData.rect;
let dir = aData.dir;
let type = aData.type;
@@ -135,13 +139,23 @@ this.DateTimePickerHelper = {
debug("aBrowser.dateTimePicker not found, exiting now.");
return;
}
- this.picker.loadPicker(type, detail);
+ // The datetimepopup binding is only attached when it is needed.
+ // Check if openPicker method is present to determine if binding has
+ // been attached. If not, attach the binding first before calling it.
+ if (!this.picker.openPicker) {
+ let bindingPromise = new Promise(resolve => {
+ this.picker.addEventListener("DateTimePickerBindingReady",
+ resolve, {once: true});
+ });
+ this.picker.setAttribute("active", true);
+ yield bindingPromise;
+ }
// The arrow panel needs an anchor to work. The popupAnchor (this._anchor)
// is a transparent div that the arrow can point to.
- this.picker.openPopup(this._anchor, "after_start", 0, 0);
+ this.picker.openPicker(type, this._anchor, detail);
this.addPickerListeners();
- },
+ }),
// Picker is closed, do some cleanup.
close: function() {
diff --git a/toolkit/themes/shared/datetimeinputpickers.css b/toolkit/themes/shared/datetimeinputpickers.css
index 741f15281..a0c046f6f 100644
--- a/toolkit/themes/shared/datetimeinputpickers.css
+++ b/toolkit/themes/shared/datetimeinputpickers.css
@@ -12,7 +12,8 @@
--colon-width: 2rem;
--day-period-spacing-width: 1rem;
--calendar-width: 23.1rem;
- --date-picker-item-height: 2.4rem;
+ --date-picker-item-height: 2.5rem;
+ --date-picker-item-width: 3.3rem;
--border: 0.1rem solid #D6D6D6;
--border-radius: 0.3rem;
@@ -21,6 +22,8 @@
--font-color: #191919;
--fill-color: #EBEBEB;
+ --today-fill-color: rgb(212, 212, 212);
+
--selected-font-color: #FFFFFF;
--selected-fill-color: #0996F8;
@@ -29,10 +32,16 @@
--button-font-color-active: #191919;
--button-fill-color-active: #D4D4D4;
- --weekday-font-color: #6C6C6C;
- --weekday-outside-font-color: #6C6C6C;
- --weekend-font-color: #DA4E44;
- --weekend-outside-font-color: #FF988F;
+ --weekday-header-font-color: #6C6C6C;
+ --weekend-header-font-color: rgb(218, 78, 68);
+
+ --weekend-font-color: rgb(218, 78, 68);
+ --weekday-outside-font-color: rgb(153, 153, 153);
+ --weekend-outside-font-color: rgb(255, 152, 143);
+
+ --weekday-disabled-font-color: rgba(25, 25, 25, 0.2);
+ --weekend-disabled-font-color: rgba(218, 78, 68, 0.2);
+ --disabled-fill-color: rgba(235, 235, 235, 0.8);
--disabled-opacity: 0.2;
}
@@ -181,11 +190,11 @@ button.month-year.active::after {
}
.week-header > div {
- color: var(--weekday-font-color);
+ color: var(--weekday-header-font-color);
}
.week-header > div.weekend {
- color: var(--weekend-font-color);
+ color: var(--weekend-header-font-color);
}
.days-viewport {
@@ -206,24 +215,49 @@ button.month-year.active::after {
align-items: center;
display: flex;
height: var(--date-picker-item-height);
- margin: 0.05rem 0.15rem;
position: relative;
justify-content: center;
- width: 3rem;
+ width: var(--date-picker-item-width);
}
-.days-view > div.outside {
+.days-view > .outside {
color: var(--weekday-outside-font-color);
}
-.days-view > div.weekend {
+.days-view > .weekend {
color: var(--weekend-font-color);
}
-.days-view > div.weekend.outside {
+.days-view > .weekend.outside {
color: var(--weekend-outside-font-color);
}
+.days-view > .out-of-range {
+ color: var(--weekday-disabled-font-color);
+ background: var(--disabled-fill-color);
+}
+
+.days-view > .out-of-range.weekend {
+ color: var(--weekend-disabled-font-color);
+}
+
+.days-view > .today {
+ font-weight: bold;
+}
+
+.days-view > .out-of-range::before {
+ display: none;
+}
+
+.days-view > div:hover::before,
+.days-view > .select::before,
+.days-view > .today::before {
+ top: 5%;
+ bottom: 5%;
+ left: 5%;
+ right: 5%;
+}
+
#time-picker,
.month-year-view {
display: flex;
@@ -283,22 +317,31 @@ button.month-year.active::after {
scroll-snap-coordinate: 0 0;
}
+.spinner-container > .spinner > div::before,
+.calendar-container .days-view > div::before {
+ position: absolute;
+ top: 5%;
+ bottom: 5%;
+ left: 5%;
+ right: 5%;
+ z-index: -10;
+ border-radius: var(--border-radius);
+}
+
.spinner-container > .spinner > div:hover::before,
.calendar-container .days-view > div:hover::before {
background: var(--fill-color);
border: var(--border);
- border-radius: var(--border-radius);
content: "";
- position: absolute;
- top: 0%;
- bottom: 0%;
- left: 0%;
- right: 0%;
- z-index: -10;
+}
+
+.calendar-container .days-view > div.today::before {
+ background: var(--today-fill-color);
+ content: "";
}
.spinner-container > .spinner:not(.scrolling) > div.selection,
-.calendar-container .days-view > div.selection {
+.calendar-container .days-view > div.selection:not(.out-of-range) {
color: var(--selected-font-color);
}
@@ -306,14 +349,7 @@ button.month-year.active::after {
.calendar-container .days-view > div.selection::before {
background: var(--selected-fill-color);
border: none;
- border-radius: var(--border-radius);
content: "";
- position: absolute;
- top: 0%;
- bottom: 0%;
- left: 0%;
- right: 0%;
- z-index: -10;
}
.spinner-container > .spinner > div.disabled::before,