diff options
35 files changed, 2346 insertions, 380 deletions
diff --git a/accessible/tests/mochitest/jsat/test_output.html b/accessible/tests/mochitest/jsat/test_output.html index ec2b289be..525642607 100644 --- a/accessible/tests/mochitest/jsat/test_output.html +++ b/accessible/tests/mochitest/jsat/test_output.html @@ -125,14 +125,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=753984 {"string": "listAbbr"}, {"string": "cellInfoAbbr", "args": [ 1, 1]}]] }, { - accOrElmOrID: "date", - expectedUtterance: [[{"string": "textInputType_date"}, - {"string": "entry"}, "2011-09-29"], ["2011-09-29", - {"string": "textInputType_date"}, {"string": "entry"}]], - expectedBraille: [[{"string": "textInputType_date"}, - {"string": "entryAbbr"}, "2011-09-29"], ["2011-09-29", - {"string": "textInputType_date"}, {"string": "entryAbbr"}]] - }, { accOrElmOrID: "email", expectedUtterance: [[{"string": "textInputType_email"}, {"string": "entry"}, "test@example.com"], ["test@example.com", @@ -619,7 +611,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=753984 <label for="password">Secret Password</label><input id="password" type="password"></input> <label for="radio_unselected">any old radio button</label><input id="radio_unselected" type="radio"></input> <label for="radio_selected">a unique radio button</label><input id="radio_selected" type="radio" checked></input> - <input id="date" type="date" value="2011-09-29" /> <input id="email" type="email" value="test@example.com" /> <input id="search" type="search" value="This is a search" /> <input id="tel" type="tel" value="555-5555" /> diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index a05b031b2..7557d15a1 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -815,7 +815,6 @@ html|*#fullscreen-exit-button { .popup-anchor { /* should occupy space but not be visible */ opacity: 0; - visibility: hidden; pointer-events: none; -moz-stack-sizing: ignore; } diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index d46eccdbc..e8a4af454 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -2832,7 +2832,8 @@ HTMLInputElement::GetOwnerDateTimeControl() HTMLInputElement::FromContentOrNull( GetParent()->GetParent()->GetParent()->GetParent()); if (ownerDateTimeControl && - ownerDateTimeControl->mType == NS_FORM_INPUT_TIME) { + (ownerDateTimeControl->mType == NS_FORM_INPUT_TIME || + ownerDateTimeControl->mType == NS_FORM_INPUT_DATE)) { return ownerDateTimeControl; } } @@ -3282,7 +3283,8 @@ HTMLInputElement::SetValueInternal(const nsAString& aValue, uint32_t aFlags) if (frame) { frame->UpdateForValueChange(); } - } else if (mType == NS_FORM_INPUT_TIME && + } else if ((mType == NS_FORM_INPUT_TIME || + mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { @@ -3591,7 +3593,8 @@ HTMLInputElement::Blur(ErrorResult& aError) } } - if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) { + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && + !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleBlurEvent(); @@ -3618,7 +3621,8 @@ HTMLInputElement::Focus(ErrorResult& aError) } } - if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) { + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && + !IsExperimentalMobileType(mType)) { nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->HandleFocusEvent(); @@ -3956,7 +3960,7 @@ HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor) } } - if (mType == NS_FORM_INPUT_TIME && + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && !IsExperimentalMobileType(mType) && aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->mOriginalTarget == this) { @@ -4083,7 +4087,8 @@ HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor) // Stop the event if the related target's first non-native ancestor is the // same as the original target's first non-native ancestor (we are moving // inside of the same element). - if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType) && + if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) && + !IsExperimentalMobileType(mType) && (aVisitor.mEvent->mMessage == eFocus || aVisitor.mEvent->mMessage == eFocusIn || aVisitor.mEvent->mMessage == eFocusOut || @@ -7161,13 +7166,15 @@ HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, int32_t* if (mType == NS_FORM_INPUT_FILE || mType == NS_FORM_INPUT_NUMBER || - mType == NS_FORM_INPUT_TIME) { + mType == NS_FORM_INPUT_TIME || + mType == NS_FORM_INPUT_DATE) { if (aTabIndex) { // We only want our native anonymous child to be tabable to, not ourself. *aTabIndex = -1; } if (mType == NS_FORM_INPUT_NUMBER || - mType == NS_FORM_INPUT_TIME) { + mType == NS_FORM_INPUT_TIME || + mType == NS_FORM_INPUT_DATE) { *aIsFocusable = true; } else { *aIsFocusable = defaultFocusable; diff --git a/dom/html/nsIFormControl.h b/dom/html/nsIFormControl.h index aaa92146c..e07f7c829 100644 --- a/dom/html/nsIFormControl.h +++ b/dom/html/nsIFormControl.h @@ -270,8 +270,8 @@ nsIFormControl::IsSingleLineTextControl(bool aExcludePassword, uint32_t aType) #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) // On Android/B2G, date/time input appears as a normal text box. aType == NS_FORM_INPUT_TIME || -#endif aType == NS_FORM_INPUT_DATE || +#endif aType == NS_FORM_INPUT_MONTH || aType == NS_FORM_INPUT_WEEK || aType == NS_FORM_INPUT_DATETIME_LOCAL || diff --git a/dom/html/test/forms/mochitest.ini b/dom/html/test/forms/mochitest.ini index 35955b189..6fceefd98 100644 --- a/dom/html/test/forms/mochitest.ini +++ b/dom/html/test/forms/mochitest.ini @@ -30,8 +30,12 @@ skip-if = os == "android" # up/down arrow keys not supported on android skip-if = android_version == '18' # Android, bug 1147974 [test_input_color_picker_update.html] skip-if = android_version == '18' # Android, bug 1147974 +[test_input_date_key_events.html] +skip-if = os == "android" [test_input_datetime_focus_blur.html] skip-if = os == "android" +[test_input_datetime_focus_blur_events.html] +skip-if = os == "android" [test_input_datetime_tabindex.html] skip-if = os == "android" [test_input_defaultValue.html] @@ -61,8 +65,6 @@ skip-if = os == "android" [test_input_textarea_set_value_no_scroll.html] [test_input_time_key_events.html] skip-if = os == "android" -[test_input_time_focus_blur_events.html] -skip-if = os == "android" [test_input_types_pref.html] [test_input_typing_sanitization.html] [test_input_untrusted_key_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 new file mode 100644 index 000000000..cd974e505 --- /dev/null +++ b/dom/html/test/forms/test_input_date_key_events.html @@ -0,0 +1,228 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1286182 +--> +<head> + <title>Test key events for time control</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286182">Mozilla Bug 1286182</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="date"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +// Turn off Spatial Navigation because it hijacks arrow keydown events: +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, function() { + test(); + SimpleTest.finish(); + }); +}); + +var testData = [ + /** + * keys: keys to send to the input element. + * initialVal: initial value set to the input element. + * expectedVal: expected value of the input element after sending the keys. + */ + { + // Type 11222016, default order is month, day, year. + keys: ["11222016"], + initialVal: "", + expectedVal: "2016-11-22" + }, + { + // Type 3 in the month field will automatically advance to the day field, + // then type 5 in the day field will automatically advance to the year + // field. + keys: ["352016"], + initialVal: "", + expectedVal: "2016-03-05" + }, + { + // Type 13 in the month field will set it to the maximum month, which is + // 12. + keys: ["13012016"], + initialVal: "", + expectedVal: "2016-12-01" + }, + { + // Type 00 in the month field will set it to the minimum month, which is 1. + keys: ["00012016"], + initialVal: "", + expectedVal: "2016-01-01" + }, + { + // Type 33 in the day field will set it to the maximum day, which is 31. + keys: ["12332016"], + initialVal: "", + expectedVal: "2016-12-31" + }, + { + // Type 00 in the day field will set it to the minimum day, which is 1. + keys: ["12002016"], + initialVal: "", + expectedVal: "2016-12-01" + }, + { + // Type 275769 in the year field will set it to the maximum year, which is + // 275760. + keys: ["0101275769"], + initialVal: "", + expectedVal: "275760-01-01" + }, + { + // Type 000000 in the year field will set it to the minimum year, which is + // 0001. + keys: ["0101000000"], + initialVal: "", + expectedVal: "0001-01-01" + }, + { + // Advance to year field and decrement. + keys: ["VK_TAB", "VK_TAB", "VK_DOWN"], + initialVal: "2016-11-25", + expectedVal: "2015-11-25" + }, + { + // Right key should do the same thing as TAB key. + keys: ["VK_RIGHT", "VK_RIGHT", "VK_DOWN"], + initialVal: "2016-11-25", + expectedVal: "2015-11-25" + }, + { + // Advance to day field then back to month field and decrement. + keys: ["VK_RIGHT", "VK_LEFT", "VK_DOWN"], + initialVal: "2000-05-01", + expectedVal: "2000-04-01" + }, + { + // Focus starts on the first field, month in this case, and increment. + keys: ["VK_UP"], + initialVal: "2000-03-01", + expectedVal: "2000-04-01" + }, + { + // Advance to day field and decrement. + keys: ["VK_TAB", "VK_DOWN"], + initialVal: "1234-01-01", + expectedVal: "1234-01-31" + }, + { + // Advance to day field and increment. + keys: ["VK_TAB", "VK_UP"], + initialVal: "1234-01-01", + expectedVal: "1234-01-02" + }, + { + // PageUp on month field increments month by 3. + keys: ["VK_PAGE_UP"], + initialVal: "1999-01-01", + expectedVal: "1999-04-01" + }, + { + // PageDown on month field decrements month by 3. + keys: ["VK_PAGE_DOWN"], + initialVal: "1999-01-01", + expectedVal: "1999-10-01" + }, + { + // PageUp on day field increments day by 7. + keys: ["VK_TAB", "VK_PAGE_UP"], + initialVal: "1999-01-01", + expectedVal: "1999-01-08" + }, + { + // PageDown on day field decrements day by 7. + keys: ["VK_TAB", "VK_PAGE_DOWN"], + initialVal: "1999-01-01", + expectedVal: "1999-01-25" + }, + { + // PageUp on year field increments year by 10. + keys: ["VK_TAB", "VK_TAB", "VK_PAGE_UP"], + initialVal: "1999-01-01", + expectedVal: "2009-01-01" + }, + { + // PageDown on year field decrements year by 10. + keys: ["VK_TAB", "VK_TAB", "VK_PAGE_DOWN"], + initialVal: "1999-01-01", + expectedVal: "1989-01-01" + }, + { + // Home key on month field sets it to the minimum month, which is 01. + keys: ["VK_HOME"], + initialVal: "2016-06-01", + expectedVal: "2016-01-01" + }, + { + // End key on month field sets it to the maximum month, which is 12. + keys: ["VK_END"], + initialVal: "2016-06-01", + expectedVal: "2016-12-01" + }, + { + // Home key on day field sets it to the minimum day, which is 01. + keys: ["VK_TAB", "VK_HOME"], + initialVal: "2016-01-10", + expectedVal: "2016-01-01" + }, + { + // End key on day field sets it to the maximum day, which is 31. + keys: ["VK_TAB", "VK_END"], + initialVal: "2016-01-10", + expectedVal: "2016-01-31" + }, + { + // Home key on year field sets it to the minimum year, which is 0001. + keys: ["VK_TAB", "VK_TAB", "VK_HOME"], + initialVal: "2016-01-01", + expectedVal: "0001-01-01" + }, + { + // End key on year field sets it to the maximum year, which is 275760. + keys: ["VK_TAB", "VK_TAB", "VK_END"], + initialVal: "2016-01-01", + expectedVal: "275760-01-01" + }, +]; + +function sendKeys(aKeys) { + for (let i = 0; i < aKeys.length; i++) { + let key = aKeys[i]; + if (key.startsWith("VK")) { + synthesizeKey(key, {}); + } else { + sendString(key); + } + } +} + +function test() { + var elem = document.getElementById("input"); + + for (let { keys, initialVal, expectedVal } of testData) { + elem.focus(); + elem.value = initialVal; + sendKeys(keys); + elem.blur(); + is(elem.value, expectedVal, + "Test with " + keys + ", result should be " + expectedVal); + elem.value = ""; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_focus_blur.html b/dom/html/test/forms/test_input_datetime_focus_blur.html index 5b8d95b25..85f7b4bb4 100644 --- a/dom/html/test/forms/test_input_datetime_focus_blur.html +++ b/dom/html/test/forms/test_input_datetime_focus_blur.html @@ -4,7 +4,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 --> <head> - <title>Test focus/blur behaviour for <input type='time'></title> + <title>Test focus/blur behaviour for date/time input types</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> </head> @@ -12,7 +12,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a> <p id="display"></p> <div id="content"> - <input id="input" type="time"> + <input id="input_time" type="time"> + <input id="input_date" type="date"> </div> <pre id="test"> <script type="application/javascript"> @@ -30,15 +31,15 @@ SimpleTest.waitForFocus(function() { SimpleTest.finish(); }); -function test() { - let time = document.getElementById("input"); - time.focus(); +function testFocusBlur(type) { + let input = document.getElementById("input_" + type); + input.focus(); - // The active element returns the input type=time. + // The active element returns the date/time input element. let activeElement = document.activeElement; - is(activeElement, time, "activeElement should be the time element"); + is(activeElement, input, "activeElement should be the date/time input element"); is(activeElement.localName, "input", "activeElement should be an input element"); - is(activeElement.type, "time", "activeElement should be of type time"); + is(activeElement.type, type, "activeElement should be of type " + type); // Use FocusManager to check that the actual focus is on the anonymous // text control. @@ -48,10 +49,17 @@ function test() { is(focusedElement.localName, "input", "focusedElement should be an input element"); is(focusedElement.type, "text", "focusedElement should be of type text"); - time.blur(); - isnot(document.activeElement, time, "activeElement should no longer be the time element"); + input.blur(); + isnot(document.activeElement, input, "activeElement should no longer be the datetime input element"); } +function test() { + let inputTypes = ["time", "date"]; + + for (let i = 0; i < inputTypes.length; i++) { + testFocusBlur(inputTypes[i]); + } +} </script> </pre> </body> diff --git a/dom/html/test/forms/test_input_datetime_focus_blur_events.html b/dom/html/test/forms/test_input_datetime_focus_blur_events.html new file mode 100644 index 000000000..873dda627 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_focus_blur_events.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1301306 +--> +<head> +<title>Test for Bug 1301306</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=1301306">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onfocus="++focusEvents[0]" + onblur="++blurEvents[0]" onfocusin="++focusInEvents[0]" + onfocusout="++focusOutEvents[0]"> +<input type="date" id="input_date" onfocus="++focusEvents[1]" + onblur="++blurEvents[1]" onfocusin="++focusInEvents[1]" + onfocusout="++focusOutEvents[1]"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** + * Test for Bug 1301306. + * This test checks that when moving inside the time input element, e.g. jumping + * through the inner text boxes, does not fire extra focus/blur events. + **/ + +var inputTypes = ["time", "date"]; +var focusEvents = [0, 0]; +var focusInEvents = [0, 0]; +var focusOutEvents = [0, 0]; +var blurEvents = [0, 0]; + +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]); + + input.focus(); + is(focusEvents[i], 1, inputTypes[i] + " input element should have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + // Move around inside the input element's input box. + synthesizeKey("VK_TAB", {}); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " time input element should not have dispatched blur event."); + + synthesizeKey("VK_RIGHT", {}); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + synthesizeKey("VK_LEFT", {}); + is(focusEvents[i], 1,inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + synthesizeKey("VK_RIGHT", {}); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + input.blur(); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 1, inputTypes[i] + " input element should have dispatched focusout event."); + is(blurEvents[i], 1, inputTypes[i] + " input element should have dispatched blur event."); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_tabindex.html b/dom/html/test/forms/test_input_datetime_tabindex.html index fb7c9b2f1..8023ccf9b 100644 --- a/dom/html/test/forms/test_input_datetime_tabindex.html +++ b/dom/html/test/forms/test_input_datetime_tabindex.html @@ -4,7 +4,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 --> <head> - <title>Test tabindex attribute for <input type='time'></title> + <title>Test tabindex attribute for date/time input types</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> @@ -16,13 +16,16 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 <input id="time1" type="time" tabindex="0"> <input id="time2" type="time" tabindex="-1"> <input id="time3" type="time" tabindex="0"> + <input id="date1" type="date" tabindex="0"> + <input id="date2" type="date" tabindex="-1"> + <input id="date3" type="date" tabindex="0"> </div> <pre id="test"> <script type="application/javascript"> /** * Test for Bug 1288591. - * This test checks whether date/time input types' tabindex attribute works + * This test checks whether date/time input types tabindex attribute works * correctly. **/ SimpleTest.waitForExplicitFinish(); @@ -31,41 +34,49 @@ SimpleTest.waitForFocus(function() { SimpleTest.finish(); }); -function test() { - let time1 = document.getElementById("time1"); - let time2 = document.getElementById("time2"); - let time3 = document.getElementById("time3"); +function testTabindex(type) { + let input1 = document.getElementById(type + "1"); + let input2 = document.getElementById(type + "2"); + let input3 = document.getElementById(type + "3"); - time1.focus(); - is(document.activeElement, time1, + input1.focus(); + is(document.activeElement, input1, "input element with tabindex=0 is focusable"); - // Advance to time1 minute field + // Advance to next inner field synthesizeKey("VK_TAB", {}); - is(document.activeElement, time1, + is(document.activeElement, input1, "input element with tabindex=0 is tabbable"); - // Advance to time1 AM/PM field + // Advance to next inner field synthesizeKey("VK_TAB", {}); - is(document.activeElement, time1, + is(document.activeElement, input1, "input element with tabindex=0 is tabbable"); // Advance to next element synthesizeKey("VK_TAB", {}); - is(document.activeElement, time3, + is(document.activeElement, input3, "input element with tabindex=-1 is not tabbable"); - time2.focus(); - is(document.activeElement, time2, + input2.focus(); + is(document.activeElement, input2, "input element with tabindex=-1 is still focusable"); // Changing the tabindex attribute dynamically. - time3.setAttribute("tabindex", "-1"); - synthesizeKey("VK_TAB", {}); // need only one TAB since time2 is not tabbable - isnot(document.activeElement, time3, + input3.setAttribute("tabindex", "-1"); + synthesizeKey("VK_TAB", {}); // need only one TAB since input2 is not tabbable + isnot(document.activeElement, input3, "element with tabindex changed to -1 should not be tabbable"); } +function test() { + let inputTypes = ["time", "date"]; + + for (let i = 0; i < inputTypes.length; i++) { + testTabindex(inputTypes[i]); + } +} + </script> </pre> </body> diff --git a/dom/html/test/forms/test_input_time_focus_blur_events.html b/dom/html/test/forms/test_input_time_focus_blur_events.html deleted file mode 100644 index 483741477..000000000 --- a/dom/html/test/forms/test_input_time_focus_blur_events.html +++ /dev/null @@ -1,82 +0,0 @@ -<!DOCTYPE HTML> -<html> -<!-- -https://bugzilla.mozilla.org/show_bug.cgi?id=1301306 ---> -<head> -<title>Test for Bug 1301306</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=1301306">Mozilla Bug 722599</a> -<p id="display"></p> -<div id="content"> -<input type="time" id="input_time" onfocus="++focusEvent" onblur="++blurEvent" - onfocusin="++focusInEvent" onfocusout="++focusOutEvent"> -</div> -<pre id="test"> -<script class="testbody" type="text/javascript"> - -/** - * Test for Bug 1301306. - * This test checks that when moving inside the time input element, e.g. jumping - * through the inner text boxes, does not fire extra focus/blur events. - **/ - -var focusEvent = 0; -var focusInEvent = 0; -var focusOutEvent = 0; -var blurEvent = 0; - -SimpleTest.waitForExplicitFinish(); -SimpleTest.waitForFocus(function() { - test(); - SimpleTest.finish(); -}); - -function test() { - var time = document.getElementById("input_time"); - time.focus(); - is(focusEvent, 1, "time input element should have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - // Move around inside the input element's input box. - synthesizeKey("VK_TAB", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - synthesizeKey("VK_RIGHT", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - synthesizeKey("VK_LEFT", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - synthesizeKey("VK_RIGHT", {}); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 0, "time input element should not have dispatched focusout event."); - is(blurEvent, 0, "time input element should not have dispatched blur event."); - - time.blur(); - is(focusEvent, 1, "time input element should not have dispatched focus event."); - is(focusInEvent, 1, "time input element should have dispatched focusin event."); - is(focusOutEvent, 1, "time input element should not have dispatched focusout event."); - is(blurEvent, 1, "time input element should have dispatched blur event."); -} - -</script> -</pre> -</body> -</html> diff --git a/dom/html/test/forms/test_input_typing_sanitization.html b/dom/html/test/forms/test_input_typing_sanitization.html index 0896f19df..eee300b33 100644 --- a/dom/html/test/forms/test_input_typing_sanitization.html +++ b/dom/html/test/forms/test_input_typing_sanitization.html @@ -26,6 +26,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=765772 * This test checks that when a user types in some input types, it will not be * in a state where the value will be un-sanitized and usable (by a script). */ +const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); var input = document.getElementById('i'); var form = document.getElementById('f'); @@ -143,6 +144,7 @@ function runTest() ] }, { + mobileOnly: true, type: 'date', validData: [ '0001-01-01', @@ -161,6 +163,28 @@ function runTest() ] }, { + mobileOnly: true, + type: 'time', + validData: [ + '00:00', + '09:09:00', + '08:23:23.1', + '21:43:56.12', + '23:12:45.100', + ], + invalidData: [ + '00:', + '00:00:', + '25:00', + '-00:00', + '00:00:00.', + '00:60', + '10:58:99', + ':19:10', + '23:08:09.1012', + ] + }, + { type: 'month', validData: [ '0001-01', @@ -218,6 +242,10 @@ function runTest() for (test of data) { gCurrentTest = test; + if (gCurrentTest.mobileOnly && isDesktop) { + continue; + } + input.type = test.type; gValidData = test.validData; gInvalidData = test.invalidData; diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl index 050d19510..b2546fcca 100644 --- a/dom/webidl/HTMLInputElement.webidl +++ b/dom/webidl/HTMLInputElement.webidl @@ -238,6 +238,9 @@ partial interface HTMLInputElement { dictionary DateTimeValue { long hour; long minute; + long year; + long month; + long day; }; partial interface HTMLInputElement { diff --git a/layout/base/nsCSSFrameConstructor.cpp b/layout/base/nsCSSFrameConstructor.cpp index a118c38f9..f8c7f52a9 100644 --- a/layout/base/nsCSSFrameConstructor.cpp +++ b/layout/base/nsCSSFrameConstructor.cpp @@ -3658,13 +3658,13 @@ nsCSSFrameConstructor::FindInputData(Element* aElement, nsCSSAnonBoxes::buttonContent) }, // TODO: this is temporary until a frame is written: bug 635240. SIMPLE_INT_CREATE(NS_FORM_INPUT_NUMBER, NS_NewNumberControlFrame), - // TODO: this is temporary until a frame is written: bug 888320. - SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewTextControlFrame), #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) // On Android/B2G, date/time input appears as a normal text box. SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewTextControlFrame), + SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewTextControlFrame), #else SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewDateTimeControlFrame), + SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewDateTimeControlFrame), #endif // TODO: this is temporary until a frame is written: bug 888320 SIMPLE_INT_CREATE(NS_FORM_INPUT_MONTH, NS_NewTextControlFrame), diff --git a/layout/forms/nsDateTimeControlFrame.cpp b/layout/forms/nsDateTimeControlFrame.cpp index df2e43986..fa22dceba 100644 --- a/layout/forms/nsDateTimeControlFrame.cpp +++ b/layout/forms/nsDateTimeControlFrame.cpp @@ -372,7 +372,8 @@ nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID, auto contentAsInputElem = static_cast<dom::HTMLInputElement*>(mContent); // If script changed the <input>'s type before setting these attributes // then we don't need to do anything since we are going to be reframed. - if (contentAsInputElem->GetType() == NS_FORM_INPUT_TIME) { + if (contentAsInputElem->GetType() == NS_FORM_INPUT_TIME || + contentAsInputElem->GetType() == NS_FORM_INPUT_DATE) { if (aAttribute == nsGkAtoms::value) { nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = do_QueryInterface(mInputAreaContent); diff --git a/layout/style/res/html.css b/layout/style/res/html.css index a779461de..bc3f08210 100644 --- a/layout/style/res/html.css +++ b/layout/style/res/html.css @@ -774,6 +774,11 @@ input[type="time"] > xul|datetimebox { -moz-binding: url("chrome://global/content/bindings/datetimebox.xml#time-input"); } +input[type="date"] > xul|datetimebox { + display: flex; + -moz-binding: url("chrome://global/content/bindings/datetimebox.xml#date-input"); +} + /* details & summary */ /* Need to revert Bug 1259889 Part 2 when removing details preference. */ @supports -moz-bool-pref("dom.details_element.enabled") { diff --git a/toolkit/components/satchel/test/test_form_autocomplete.html b/toolkit/components/satchel/test/test_form_autocomplete.html index 4cf09117a..d2c22a3db 100644 --- a/toolkit/components/satchel/test/test_form_autocomplete.html +++ b/toolkit/components/satchel/test/test_form_autocomplete.html @@ -172,7 +172,7 @@ function setupFormHistory(aCallback) { { op : "add", fieldname : "field8", value : "value" }, { op : "add", fieldname : "field9", value : "value" }, { op : "add", fieldname : "field10", value : "42" }, - { op : "add", fieldname : "field11", value : "2010-10-10" }, + { op : "add", fieldname : "field11", value : "2010-10-10" }, // not used, since type=date doesn't have autocomplete currently { op : "add", fieldname : "field12", value : "21:21" }, // not used, since type=time doesn't have autocomplete currently { op : "add", fieldname : "field13", value : "32" }, // not used, since type=range doesn't have a drop down menu { op : "add", fieldname : "field14", value : "#ffffff" }, // not used, since type=color doesn't have autocomplete currently @@ -899,15 +899,13 @@ function runTest() { input = $_(14, "field11"); restoreForm(); - expectPopup(); - doKey("down"); + waitForMenuChange(0); break; case 405: - checkMenuEntries(["2010-10-10"]); - doKey("down"); - doKey("return"); - checkForm("2010-10-10"); + checkMenuEntries([]); // type=date with it's own control frame does not + // have a drop down menu for now + checkForm(""); input = $_(15, "field12"); restoreForm(); diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index 4ae798fbd..731b55185 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1737,6 +1737,7 @@ let DateTimePickerListener = { } case "MozUpdateDateTimePicker": { let value = this._inputElement.getDateTimeInputBoxValue(); + value.type = this._inputElement.type; sendAsyncMessage("FormDateTime:UpdatePicker", { value }); break; } diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml new file mode 100644 index 000000000..4da6e398f --- /dev/null +++ b/toolkit/content/datepicker.xhtml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <title>Date Picker</title> + <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/> + <script type="application/javascript" src="chrome://global/content/bindings/datekeeper.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/calendar.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/datepicker.js"></script> +</head> +<body> + <div id="date-picker"> + <div class="calendar-container"> + <div class="nav"> + <button class="left"/> + <button class="right"/> + </div> + <div class="week-header"></div> + <div class="days-viewport"> + <div class="days-view"></div> + </div> + </div> + <div class="month-year-container"> + <button class="month-year"/> + </div> + <div class="month-year-view"></div> + </div> + <template id="spinner-template"> + <div class="spinner-container"> + <button class="up"/> + <div class="spinner"></div> + <button class="down"/> + </div> + </template> + <script type="application/javascript"> + // We need to hide the scroll bar but maintain its scrolling + // capability, so using |overflow: hidden| is not an option. + // Instead, we are inserting a user agent stylesheet that is + // capable of selecting scrollbars, and do |display: none|. + var domWinUtls = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + domWinUtls.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbar { display: none; }', domWinUtls.AGENT_SHEET); + // Create a DatePicker instance and prepare to be + // initialized by the "DatePickerInit" event from datetimepopup.xml + const root = document.getElementById("date-picker"); + new DatePicker({ + monthYear: root.querySelector(".month-year"), + monthYearView: root.querySelector(".month-year-view"), + buttonLeft: root.querySelector(".left"), + buttonRight: root.querySelector(".right"), + weekHeader: root.querySelector(".week-header"), + daysView: root.querySelector(".days-view") + }); + </script> +</body> +</html>
\ No newline at end of file diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn index 0a0f0253b..538e42952 100644 --- a/toolkit/content/jar.mn +++ b/toolkit/content/jar.mn @@ -45,6 +45,7 @@ toolkit.jar: content/global/customizeToolbar.js content/global/customizeToolbar.xul #endif + content/global/datepicker.xhtml #ifndef MOZ_FENNEC content/global/editMenuOverlay.js * content/global/editMenuOverlay.xul @@ -75,8 +76,11 @@ toolkit.jar: content/global/bindings/autocomplete.xml (widgets/autocomplete.xml) content/global/bindings/browser.xml (widgets/browser.xml) content/global/bindings/button.xml (widgets/button.xml) + content/global/bindings/calendar.js (widgets/calendar.js) content/global/bindings/checkbox.xml (widgets/checkbox.xml) content/global/bindings/colorpicker.xml (widgets/colorpicker.xml) + content/global/bindings/datekeeper.js (widgets/datekeeper.js) + content/global/bindings/datepicker.js (widgets/datepicker.js) content/global/bindings/datetimepicker.xml (widgets/datetimepicker.xml) content/global/bindings/datetimepopup.xml (widgets/datetimepopup.xml) content/global/bindings/datetimebox.xml (widgets/datetimebox.xml) diff --git a/toolkit/content/timepicker.xhtml b/toolkit/content/timepicker.xhtml index 1396223f1..77b9fba41 100644 --- a/toolkit/content/timepicker.xhtml +++ b/toolkit/content/timepicker.xhtml @@ -6,7 +6,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <head> <title>Time Picker</title> - <link rel="stylesheet" href="chrome://global/skin/timepicker.css"/> + <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/> <script type="application/javascript" src="chrome://global/content/bindings/timekeeper.js"></script> <script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script> <script type="application/javascript" src="chrome://global/content/bindings/timepicker.js"></script> diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js new file mode 100644 index 000000000..72e0d9d61 --- /dev/null +++ b/toolkit/content/widgets/calendar.js @@ -0,0 +1,172 @@ +/* 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"; + +/** + * Initialize the Calendar and generate nodes for week headers and days, and + * attach event listeners. + * + * @param {Object} options + * { + * {Number} calViewSize: Number of days to appear on a calendar view + * } + * @param {Object} context + * { + * {DOMElement} weekHeader + * {DOMElement} daysView + * } + */ +function Calendar(options, context) { + const DAYS_IN_A_WEEK = 7; + + this.context = context; + this.state = { + days: [], + weekHeaders: [] + }; + this.props = {}; + this.elements = { + weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader), + daysView: this._generateNodes(options.calViewSize, context.daysView) + }; + + this._attachEventListeners(); +} + +{ + Calendar.prototype = { + + /** + * Set new properties and render them. + * + * @param {Object} props + * { + * {Boolean} isVisible: Whether or not the calendar is in view + * {Array<Object>} days: Data for days + * { + * {Number} dateValue: Date in milliseconds + * {Number} textContent + * {Array<String>} classNames + * } + * {Array<Object>} weekHeaders: Data for weekHeaders + * { + * {Number} textContent + * {Array<String>} classNames + * } + * {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 + * } + */ + setProps(props) { + if (props.isVisible) { + // Transform the days and weekHeaders array for rendering + const days = props.days.map(({ dateValue, textContent, classNames }) => { + return { + dateValue, + textContent: props.getDayString(textContent), + className: dateValue == props.selectionValue ? + classNames.concat("selection").join(" ") : + classNames.join(" ") + }; + }); + const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => { + return { + textContent: props.getWeekHeaderString(textContent), + className: classNames.join(" ") + }; + }); + // Update the DOM nodes states + this._render({ + elements: this.elements.daysView, + items: days, + prevState: this.state.days + }); + this._render({ + elements: this.elements.weekHeaders, + items: weekHeaders, + prevState: this.state.weekHeaders, + }); + // Update the state to current + this.state.days = days; + this.state.weekHeaders = weekHeaders; + } + + this.props = Object.assign(this.props, props); + }, + + /** + * Render the items onto the DOM nodes + * @param {Object} + * { + * {Array<DOMElement>} elements + * {Array<Object>} items + * {Array<Object>} prevState: state of items from last render + * } + */ + _render({ elements, items, prevState }) { + for (let i = 0, l = items.length; i < l; i++) { + let el = elements[i]; + + // Check if state from last render has changed, if so, update the elements + if (!prevState[i] || prevState[i].textContent != items[i].textContent) { + el.textContent = items[i].textContent; + } + if (!prevState[i] || prevState[i].className != items[i].className) { + el.className = items[i].className; + } + } + }, + + /** + * Generate DOM nodes + * + * @param {Number} size: Number of nodes to generate + * @param {DOMElement} context: Element to append the nodes to + * @return {Array<DOMElement>} + */ + _generateNodes(size, context) { + let frag = document.createDocumentFragment(); + let refs = []; + + for (let i = 0; i < size; i++) { + let el = document.createElement("div"); + el.dataset.id = i; + refs.push(el); + frag.appendChild(el); + } + context.appendChild(frag); + + return refs; + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + 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 + }); + } + break; + } + } + }, + + /** + * Attach event listener to daysView + */ + _attachEventListeners() { + this.context.daysView.addEventListener("click", this); + } + }; +} diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js new file mode 100644 index 000000000..de01fdade --- /dev/null +++ b/toolkit/content/widgets/datekeeper.js @@ -0,0 +1,244 @@ +/* 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"; + +/** + * 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); +} + +{ + const DAYS_IN_A_WEEK = 7, + MONTHS_IN_A_YEAR = 12, + YEAR_VIEW_SIZE = 200, + YEAR_BUFFER_SIZE = 10; + + DateKeeper.prototype = { + /** + * Set new date + * @param {Object} date parts + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * } + */ + set({ year = this.state.year, month = this.state.month, day = this.state.day }) { + this._update(year, month, day); + }, + + /** + * Set date with value + * @param {Number} value: Date value + */ + setValue(value) { + const dateObj = new Date(value); + this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate()); + }, + + /** + * Set month. Makes sure the day is <= the last day of the month + * @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)); + }, + + /** + * Set year. Makes sure the day is <= the last day of the month + * @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)); + }, + + /** + * Set month by offset. Makes sure the day is <= the last day of the month + * @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(); + }, + + /** + * Generate the array of months + * @return {Array<Object>} + * { + * {Number} value: Month in int + * {Boolean} enabled + * } + */ + getMonths() { + // TODO: add min/max and step support + let months = []; + + for (let i = 0; i < MONTHS_IN_A_YEAR; i++) { + months.push({ + value: i, + enabled: true + }); + } + + return months; + }, + + /** + * Generate the array of years + * @return {Array<Object>} + * { + * {Number} value: Year in int + * {Boolean} enabled + * } + */ + 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(); + + // Generate new years array when the year is outside of the first & + // last item range. If not, return the cached result. + if (!firstItem || !lastItem || + currentYear <= firstItem.value + YEAR_BUFFER_SIZE || + currentYear >= lastItem.value - YEAR_BUFFER_SIZE) { + // The year is set in the middle with items on both directions + for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) { + years.push({ + value: currentYear + i, + enabled: true + }); + } + this.state.years = years; + } + return this.state.years; + }, + + /** + * Get days for calendar + * @return {Array<Object>} + * { + * {Number} dateValue + * {Number} textContent + * {Array<String>} classNames + * } + */ + getDays() { + // TODO: add min/max and step support + let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + 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); + let classNames = []; + if (this.state.weekends.includes(dateObj.getUTCDay())) { + classNames.push("weekend"); + } + if (month != dateObj.getUTCMonth()) { + classNames.push("outside"); + } + days.push({ + dateValue: dateObj.getTime(), + textContent: dateObj.getUTCDate(), + classNames + }); + } + return days; + }, + + /** + * Get week headers for calendar + * @param {Number} firstDayOfWeek + * @return {Array<Object>} + * { + * {Number} textContent + * {Array<String>} classNames + * } + */ + _getWeekHeaders(firstDayOfWeek) { + 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"] : [] + }); + dayOfWeek++; + } + return headers; + }, + + /** + * Get the first day on a calendar month + * @param {Date} dateObj + * @param {Number} firstDayOfWeek + * @return {Date} + */ + _getFirstCalendarDate(dateObj, firstDayOfWeek) { + const daysOffset = 1 - DAYS_IN_A_WEEK; + let firstDayOfMonth = this._newUTCDate(dateObj.getUTCFullYear(), dateObj.getUTCMonth()); + let dayOfWeek = firstDayOfMonth.getUTCDay(); + + return this._newUTCDate( + firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + // When first calendar date is the same as first day of the week, add + // another row on top of it. + firstDayOfWeek == dayOfWeek ? daysOffset : (firstDayOfWeek - dayOfWeek + daysOffset) % DAYS_IN_A_WEEK); + }, + + /** + * Helper function for creating UTC dates + * @param {...[Number]} parts + * @return {Date} + */ + _newUTCDate(...parts) { + return new Date(Date.UTC(...parts)); + } + }; +} diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js new file mode 100644 index 000000000..210ca856c --- /dev/null +++ b/toolkit/content/widgets/datepicker.js @@ -0,0 +1,383 @@ +/* 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"; + +function DatePicker(context) { + this.context = context; + this._attachEventListeners(); +} + +{ + const CAL_VIEW_SIZE = 42; + + DatePicker.prototype = { + /** + * Initializes the date picker. Set the default states and properties. + * @param {Object} props + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * {String} locale [optional]: User preferred locale + * } + */ + init(props = {}) { + this.props = props; + this._setDefaultState(); + this._createComponents(); + this._update(); + }, + + /* + * 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 dateKeeper = new DateKeeper({ + year, month, day + }, { + calViewSize: CAL_VIEW_SIZE, + firstDayOfWeek: 0, + weekends: [0] + }); + + 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; + this._update(); + this._dispatchState(); + this._closePopup(); + }, + setYear: year => { + dateKeeper.setYear(year); + this.state.isYearSet = true; + this._update(); + this._dispatchState(); + }, + setMonth: month => { + dateKeeper.setMonth(month); + this.state.isMonthSet = true; + this._update(); + this._dispatchState(); + }, + toggleMonthPicker: () => { + this.state.isMonthPickerVisible = !this.state.isMonthPickerVisible; + this._update(); + } + }; + }, + + /** + * Initalize the date picker components. + */ + _createComponents() { + this.components = { + calendar: new Calendar({ + calViewSize: CAL_VIEW_SIZE, + locale: this.state.locale + }, { + weekHeader: this.context.weekHeader, + daysView: this.context.daysView + }), + monthYear: new MonthYear({ + setYear: this.state.setYear, + setMonth: this.state.setMonth, + locale: this.state.locale + }, { + monthYear: this.context.monthYear, + monthYearView: this.context.monthYearView + }) + }; + }, + + /** + * Update date picker and its components. + */ + _update() { + const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state; + + if (isMonthPickerVisible) { + this.state.months = dateKeeper.getMonths(); + this.state.years = dateKeeper.getYears(); + } else { + this.state.days = dateKeeper.getDays(); + } + + 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 + }); + this.components.calendar.setProps({ + isVisible: !isMonthPickerVisible, + days: this.state.days, + weekHeaders: dateKeeper.state.weekHeaders, + setValue: this.state.setValue, + getDayString: this.state.getDayString, + getWeekHeaderString: this.state.getWeekHeaderString, + selectionValue + }); + + isMonthPickerVisible ? + this.context.monthYearView.classList.remove("hidden") : + this.context.monthYearView.classList.add("hidden"); + }, + + /** + * Use postMessage to close the picker. + */ + _closePopup() { + window.postMessage({ + name: "ClosePopup" + }, "*"); + }, + + /** + * 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; + // The panel is listening to window for postMessage event, so we + // do postMessage to itself to send data to input boxes. + window.postMessage({ + name: "PickerPopupChanged", + detail: { + year, + month, + day, + isYearSet, + isMonthSet, + isDaySet + } + }, "*"); + }, + + /** + * Attach event listeners + */ + _attachEventListeners() { + window.addEventListener("message", this); + document.addEventListener("mouseup", this, { passive: true }); + document.addEventListener("mousedown", this); + }, + + /** + * Handle events. + * + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "message": { + this.handleMessage(event); + break; + } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + + if (event.target == this.context.buttonLeft) { + event.target.classList.add("active"); + this.state.dateKeeper.setMonthByOffset(-1); + this._update(); + } else if (event.target == this.context.buttonRight) { + event.target.classList.add("active"); + this.state.dateKeeper.setMonthByOffset(1); + this._update(); + } + break; + } + case "mouseup": { + if (event.target == this.context.buttonLeft || event.target == this.context.buttonRight) { + event.target.classList.remove("active"); + } + + } + } + }, + + /** + * Handle postMessage events. + * + * @param {Event} event + */ + handleMessage(event) { + switch (event.data.name) { + case "PickerSetValue": { + this.set(event.data.detail); + break; + } + case "PickerInit": { + this.init(event.data.detail); + break; + } + } + }, + + /** + * Set the date state and update the components with the new state. + * + * @param {Object} dateState + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * } + */ + 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 + }); + this._update(); + } + }; + + /** + * MonthYear is a component that handles the month & year spinners + * + * @param {Object} options + * { + * {String} locale + * {Function} setYear + * {Function} setMonth + * } + * @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; + + this.context = context; + this.state = { dateFormat }; + this.props = {}; + this.components = { + month: new Spinner({ + setValue: month => { + this.state.isMonthSet = true; + options.setMonth(month); + }, + getDisplayString: month => monthFormat(new Date(0, month)), + viewportSize: spinnerSize + }, context.monthYearView), + year: new Spinner({ + setValue: year => { + this.state.isYearSet = true; + options.setYear(year); + }, + getDisplayString: year => yearFormat(new Date(new Date(0).setFullYear(year))), + viewportSize: spinnerSize + }, context.monthYearView) + }; + + this._attachEventListeners(); + } + + MonthYear.prototype = { + + /** + * Set new properties and pass them to components + * + * @param {Object} props + * { + * {Boolean} isVisible + * {Date} dateObj + * {Number} month + * {Number} year + * {Array<Object>} months + * {Array<Object>} years + * {Function} toggleMonthPicker + * } + */ + setProps(props) { + this.context.monthYear.textContent = this.state.dateFormat(props.dateObj); + + if (props.isVisible) { + this.context.monthYear.classList.add("active"); + this.components.month.setState({ + value: props.month, + items: props.months, + isInfiniteScroll: true, + isValueSet: this.state.isMonthSet, + smoothScroll: !this.state.firstOpened + }); + this.components.year.setState({ + value: props.year, + items: props.years, + isInfiniteScroll: false, + isValueSet: this.state.isYearSet, + smoothScroll: !this.state.firstOpened + }); + this.state.firstOpened = false; + } else { + this.context.monthYear.classList.remove("active"); + this.state.isMonthSet = false; + this.state.isYearSet = false; + this.state.firstOpened = true; + } + + this.props = Object.assign(this.props, props); + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + this.props.toggleMonthPicker(); + break; + } + } + }, + + /** + * Attach event listener to monthYear button + */ + _attachEventListeners() { + this.context.monthYear.addEventListener("click", this); + } + }; +} diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css index 4a9593a69..18ff024c7 100644 --- a/toolkit/content/widgets/datetimebox.css +++ b/toolkit/content/widgets/datetimebox.css @@ -20,6 +20,8 @@ border: 0; margin: 0; ime-mode: disabled; + cursor: default; + -moz-user-select: none; } .datetime-separator { diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 05591e65a..5859f80dd 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -10,6 +10,419 @@ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xbl="http://www.mozilla.org/xbl"> + <binding id="date-input" + extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base"> + <resources> + <stylesheet src="chrome://global/content/textbox.css"/> + <stylesheet src="chrome://global/skin/textbox.css"/> + <stylesheet src="chrome://global/content/bindings/datetimebox.css"/> + </resources> + + <implementation> + <constructor> + <![CDATA[ + // TODO: Bug 1320227 - [DateTimeInput] localization for + // <input type=date> input box + this.mMonthPlaceHolder = "mm"; + this.mDayPlaceHolder = "dd"; + this.mYearPlaceHolder = "yyyy"; + this.mSeparatorText = "/"; + this.mMinMonth = 1; + this.mMaxMonth = 12; + this.mMinDay = 1; + this.mMaxDay = 31; + this.mMinYear = 1; + // Maximum year limited by ECMAScript date object range, year <= 275760. + this.mMaxYear = 275760; + this.mMonthDayLength = 2; + this.mYearLength = 4; + this.mMonthPageUpDownInterval = 3; + this.mDayPageUpDownInterval = 7; + this.mYearPageUpDownInterval = 10; + + // Default to en-US, month-day-year order. + this.mMonthField = + document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + this.mDayField = + document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + this.mYearField = + document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + this.mYearField.size = this.mYearLength; + this.mYearField.maxLength = this.mMaxYear.toString().length; + + this.mMonthField.placeholder = this.mMonthPlaceHolder; + this.mDayField.placeholder = this.mDayPlaceHolder; + this.mYearField.placeholder = this.mYearPlaceHolder; + + this.mMonthField.setAttribute("min", this.mMinMonth); + this.mMonthField.setAttribute("max", this.mMaxMonth); + this.mMonthField.setAttribute("pginterval", + this.mMonthPageUpDownInterval); + this.mDayField.setAttribute("min", this.mMinDay); + this.mDayField.setAttribute("max", this.mMaxDay); + this.mDayField.setAttribute("pginterval", this.mDayPageUpDownInterval); + this.mYearField.setAttribute("min", this.mMinYear); + this.mYearField.setAttribute("max", this.mMaxYear); + this.mYearField.setAttribute("pginterval", + this.mYearPageUpDownInterval); + + this.mDaySeparator = + document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + this.mDaySeparator.textContent = this.mSeparatorText; + this.mYearSeparator = + document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + this.mYearSeparator.textContent = this.mSeparatorText; + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + ]]> + </constructor> + + <method name="clearInputFields"> + <parameter name="aFromInputElement"/> + <body> + <![CDATA[ + this.log("clearInputFields"); + + if (this.isDisabled() || this.isReadonly()) { + return; + } + + if (this.mMonthField && !this.mMonthField.disabled && + !this.mMonthField.readOnly) { + this.mMonthField.value = ""; + this.mMonthField.setAttribute("typeBuffer", ""); + } + + if (this.mDayField && !this.mDayField.disabled && + !this.mDayField.readOnly) { + this.mDayField.value = ""; + this.mDayField.setAttribute("typeBuffer", ""); + } + + if (this.mYearField && !this.mYearField.disabled && + !this.mYearField.readOnly) { + this.mYearField.value = ""; + this.mYearField.setAttribute("typeBuffer", ""); + } + + if (!aFromInputElement) { + this.mInputElement.setUserInput(""); + } + ]]> + </body> + </method> + + <method name="setFieldsFromInputValue"> + <body> + <![CDATA[ + let value = this.mInputElement.value; + if (!value) { + this.clearInputFields(true); + return; + } + + this.log("setFieldsFromInputValue: " + value); + let [year, month, day] = value.split("-"); + + this.setFieldValue(this.mYearField, year); + this.setFieldValue(this.mMonthField, month); + this.setFieldValue(this.mDayField, day); + + this.notifyPicker(); + ]]> + </body> + </method> + + <method name="getDaysInMonth"> + <parameter name="aMonth"/> + <parameter name="aYear"/> + <body> + <![CDATA[ + // Javascript's month is 0-based, so this means last day of the + // previous month. + return new Date(aYear, aMonth, 0).getDate(); + ]]> + </body> + </method> + + <method name="isFieldInvalid"> + <parameter name="aField"/> + <body> + <![CDATA[ + if (this.isEmpty(aField.value)) { + return true; + } + + let min = Number(aField.getAttribute("min")); + let max = Number(aField.getAttribute("max")); + + if (Number(aField.value) < min || Number(aField.value) > max) { + return true; + } + + return false; + ]]> + </body> + </method> + + <method name="setInputValueFromFields"> + <body> + <![CDATA[ + if (this.isFieldInvalid(this.mYearField) || + this.isFieldInvalid(this.mMonthField) || + this.isFieldInvalid(this.mDayField)) { + // We still need to notify picker in case any of the field has + // changed. If we can set input element value, then notifyPicker + // will be called in setFieldsFromInputValue(). + this.notifyPicker(); + return; + } + + let year = this.mYearField.value; + let month = this.mMonthField.value; + let day = this.mDayField.value; + + if (day > this.getDaysInMonth(month, year)) { + // Don't set invalid date, otherwise input element's value will be + // set to empty. + return; + } + + let date = [year, month, day].join("-"); + + this.log("setInputValueFromFields: " + date); + this.mInputElement.setUserInput(date); + ]]> + </body> + </method> + + <method name="setFieldsFromPicker"> + <parameter name="aValue"/> + <body> + <![CDATA[ + let year = aValue.year; + let month = aValue.month; + let day = aValue.day; + + if (!this.isEmpty(year)) { + this.setFieldValue(this.mYearField, year); + } + + if (!this.isEmpty(month)) { + this.setFieldValue(this.mMonthField, month); + } + + if (!this.isEmpty(day)) { + this.setFieldValue(this.mDayField, day); + } + ]]> + </body> + </method> + + <method name="handleKeypress"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (this.isDisabled() || this.isReadonly()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) { + let buffer = targetField.getAttribute("typeBuffer") || ""; + + buffer = buffer.concat(key); + this.setFieldValue(targetField, buffer); + targetField.select(); + + let n = Number(buffer); + let max = targetField.getAttribute("max"); + if (buffer.length >= targetField.maxLength || n * 10 > max) { + buffer = ""; + this.advanceToNextField(); + } + targetField.setAttribute("typeBuffer", buffer); + } + ]]> + </body> + </method> + + <method name="incrementFieldValue"> + <parameter name="aTargetField"/> + <parameter name="aTimes"/> + <body> + <![CDATA[ + let value; + + // Use current date if field is empty. + if (this.isEmpty(aTargetField.value)) { + let now = new Date(); + + if (aTargetField == this.mYearField) { + value = now.getFullYear(); + } else if (aTargetField == this.mMonthField) { + value = now.getMonth() + 1; + } else if (aTargetField == this.mDayField) { + value = now.getDate(); + } else { + this.log("Field not supported in incrementFieldValue."); + return; + } + } else { + value = Number(aTargetField.value); + } + + let min = Number(aTargetField.getAttribute("min")); + let max = Number(aTargetField.getAttribute("max")); + + value += Number(aTimes); + if (value > max) { + value -= (max - min + 1); + } else if (value < min) { + value += (max - min + 1); + } + this.setFieldValue(aTargetField, value); + aTargetField.select(); + ]]> + </body> + </method> + + <method name="handleKeyboardNav"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (this.isDisabled() || this.isReadonly()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + switch (key) { + case "ArrowUp": + this.incrementFieldValue(targetField, 1); + break; + case "ArrowDown": + this.incrementFieldValue(targetField, -1); + break; + case "PageUp": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, interval); + break; + } + case "PageDown": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, 0 - interval); + break; + } + case "Home": + let min = targetField.getAttribute("min"); + this.setFieldValue(targetField, min); + targetField.select(); + break; + case "End": + let max = targetField.getAttribute("max"); + this.setFieldValue(targetField, max); + targetField.select(); + break; + } + this.setInputValueFromFields(); + ]]> + </body> + </method> + + <method name="getCurrentValue"> + <body> + <![CDATA[ + let year; + if (!this.isEmpty(this.mYearField.value)) { + year = Number(this.mYearField.value); + } + + let month; + if (!this.isEmpty(this.mMonthField.value)) { + month = Number(this.mMonthField.value); + } + + let day; + if (!this.isEmpty(this.mDayField.value)) { + day = Number(this.mDayField.value); + } + + let date = { year, month, day }; + + this.log("getCurrentValue: " + JSON.stringify(date)); + return date; + ]]> + </body> + </method> + + <method name="setFieldValue"> + <parameter name="aField"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + let value = Number(aValue); + if (isNaN(value)) { + this.log("NaN on setFieldValue!"); + return; + } + + if (aValue.length == aField.maxLength) { + let min = Number(aField.getAttribute("min")); + let max = Number(aField.getAttribute("max")); + + if (aValue < min) { + value = min; + } else if (aValue > max) { + value = max; + } + } + + if (aField == this.mMonthField || + aField == this.mDayField) { + // prepend zero + if (value < 10) { + value = "0" + value; + } + } else { + // prepend zeroes + if (value < 10) { + value = "000" + value; + } else if (value < 100) { + value = "00" + value; + } else if (value < 1000) { + value = "0" + value; + } + + if (value.toString().length > this.mYearLength && + value.toString().length <= this.mMaxYear.toString().length) { + this.mYearField.size = value.toString().length; + } + } + + aField.value = value; + ]]> + </body> + </method> + + <method name="isValueAvailable"> + <body> + <![CDATA[ + return !this.isEmpty(this.mMonthField.value) || + !this.isEmpty(this.mDayField.value) || + !this.isEmpty(this.mYearField.value); + ]]> + </body> + </method> + + </implementation> + </binding> + <binding id="time-input" extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base"> <resources> @@ -282,21 +695,25 @@ if (this.mHourField && !this.mHourField.disabled && !this.mHourField.readOnly) { this.mHourField.value = ""; + this.mHourField.setAttribute("typeBuffer", ""); } if (this.mMinuteField && !this.mMinuteField.disabled && !this.mMinuteField.readOnly) { this.mMinuteField.value = ""; + this.mMinuteField.setAttribute("typeBuffer", ""); } if (this.mSecondField && !this.mSecondField.disabled && !this.mSecondField.readOnly) { this.mSecondField.value = ""; + this.mSecondField.setAttribute("typeBuffer", ""); } if (this.mMillisecField && !this.mMillisecField.disabled && !this.mMillisecField.readOnly) { this.mMillisecField.value = ""; + this.mMillisecField.setAttribute("typeBuffer", ""); } if (this.mDayPeriodField && !this.mDayPeriodField.disabled && @@ -579,9 +996,40 @@ this.mMax = this.mInputElement.max; this.mStep = this.mInputElement.step; this.mIsPickerOpen = false; + + this.EVENTS.forEach((eventName) => { + this.addEventListener(eventName, this, { mozSystemGroup: true }); + }); + // Handle keypress separately since we need to catch it on capturing. + this.addEventListener("keypress", this, { + capture: true, + mozSystemGroup: true + }); ]]> </constructor> + <destructor> + <![CDATA[ + this.mInputElement = null; + + this.EVENTS.forEach((eventName) => { + this.removeEventListener(eventName, this, { mozSystemGroup: true }); + }); + this.removeEventListener("keypress", onKeyPress, { + capture: true, + mozSystemGroup: true + }); + ]]> + </destructor> + + <property name="EVENTS" readonly="true"> + <getter> + <![CDATA[ + return ["click", "focus", "blur", "copy", "cut", "paste"]; + ]]> + </getter> + </property> + <method name="log"> <parameter name="aMsg"/> <body> @@ -710,6 +1158,12 @@ </body> </method> + <method name="getCurrentValue"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + <method name="notifyPicker"> <body> <![CDATA[ @@ -736,72 +1190,140 @@ </body> </method> - </implementation> - - <handlers> - <handler event="focus"> - <![CDATA[ - this.log("focus on: " + event.originalTarget); + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("handleEvent: " + aEvent.type); - let target = event.originalTarget; - if (target.type == "text") { - this.mLastFocusedField = target; - target.select(); - } - ]]> - </handler> + switch (aEvent.type) { + case "keypress": { + this.onKeyPress(aEvent); + break; + } + case "click": { + this.onClick(aEvent); + break; + } + case "focus": { + this.onFocus(aEvent); + break; + } + case "blur": { + this.onBlur(aEvent); + break; + } + case "copy": + case "cut": + case "paste": { + aEvent.preventDefault(); + break; + } + default: + break; + } + ]]> + </body> + </method> - <handler event="blur"> - <![CDATA[ - this.setInputValueFromFields(); - ]]> - </handler> + <method name="onFocus"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onFocus originalTarget: " + aEvent.originalTarget); - <handler event="click"> - <![CDATA[ - // 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. - this.log("click on: " + event.originalTarget); - if (event.defaultPrevented || this.isDisabled() || this.isReadonly()) { - return; - } + let target = aEvent.originalTarget; + if ((target instanceof HTMLInputElement) && target.type == "text") { + this.mLastFocusedField = target; + target.select(); + } + ]]> + </body> + </method> - if (!(event.originalTarget instanceof HTMLButtonElement)) { - this.mInputElement.openDateTimePicker(this.getCurrentValue()); - } - ]]> - </handler> + <method name="onBlur"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onBlur originalTarget: " + aEvent.originalTarget); - <handler event="keypress" phase="capturing"> - <![CDATA[ - let key = event.key; - this.log("keypress: " + key); + let target = aEvent.originalTarget; + target.setAttribute("typeBuffer", ""); + this.setInputValueFromFields(); + ]]> + </body> + </method> - if (key == "Backspace" || key == "Tab") { - return; - } + <method name="onKeyPress"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onKeyPress key: " + aEvent.key); + + switch (aEvent.key) { + // Close picker on Enter or Space key. + case "Enter": + case " ": { + this.mInputElement.closeDateTimePicker(); + aEvent.preventDefault(); + break; + } + case "Backspace": { + let targetField = aEvent.originalTarget; + targetField.setAttribute("typeBuffer", ""); + break; + } + case "ArrowRight": + case "ArrowLeft": { + this.advanceToNextField(aEvent.key == "ArrowRight" ? false : true); + aEvent.preventDefault(); + break; + } + case "ArrowUp": + case "ArrowDown": + case "PageUp": + case "PageDown": + case "Home": + case "End": { + this.handleKeyboardNav(aEvent); + aEvent.preventDefault(); + break; + } + default: { + // printable characters + if (aEvent.keyCode == 0 && + !(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)) { + this.handleKeypress(aEvent); + aEvent.preventDefault(); + } + break; + } + } + ]]> + </body> + </method> - if (key == "Enter" || key == " ") { - // Close picker on Enter and Space. - this.mInputElement.closeDateTimePicker(); - } + <method name="onClick"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.log("onClick originalTarget: " + aEvent.originalTarget); - if (key == "ArrowUp" || key == "ArrowDown" || - key == "PageUp" || key == "PageDown" || - key == "Home" || key == "End") { - this.handleKeyboardNav(event); - } else if (key == "ArrowRight" || key == "ArrowLeft") { - this.advanceToNextField((key == "ArrowRight" ? false : true)); - } else { - this.handleKeypress(event); - } + // 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. + if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) { + return; + } - event.preventDefault(); - ]]> - </handler> - </handlers> + if (!(aEvent.originalTarget instanceof HTMLButtonElement)) { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } + ]]> + </body> + </method> + </implementation> </binding> </bindings> diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 327f45368..86e8780c1 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -11,12 +11,17 @@ xmlns:xbl="http://www.mozilla.org/xbl"> <binding id="datetime-popup" extends="chrome://global/content/bindings/popup.xml#arrowpanel"> + <resources> + <stylesheet src="chrome://global/skin/datetimepopup.css"/> + </resources> <implementation> <field name="dateTimePopupFrame"> this.querySelector("#dateTimePopupFrame"); </field> <field name="TIME_PICKER_WIDTH" readonly="true">"12em"</field> <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"> <parameter name="type"/> <parameter name="detail"/> @@ -35,6 +40,14 @@ this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; break; } + case "date": { + this.detail = detail; + this.dateTimePopupFrame.addEventListener("load", this, true); + this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/datepicker.xhtml"); + this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT; + break; + } } ]]></body> </method> @@ -45,7 +58,7 @@ this.pickerState = {}; this.type = undefined; this.dateTimePopupFrame.removeEventListener("load", this, true); - this.dateTimePopupFrame.contentDocument.removeEventListener("TimePickerPopupChanged", this, false); + this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false); this.dateTimePopupFrame.setAttribute("src", ""); ]]></body> </method> @@ -55,25 +68,39 @@ switch (this.type) { case "time": { this.postMessageToPicker({ - name: "TimePickerSetValue", + name: "PickerSetValue", detail: data.value }); break; } + case "date": { + const { year, month, day } = data.value; + this.postMessageToPicker({ + name: "PickerSetValue", + detail: { + year, + // Month value from input box starts from 1 instead of 0 + month: month == undefined ? undefined : month - 1, + day + } + }); + break; + } } ]]></body> </method> <method name="initPicker"> <parameter name="detail"/> <body><![CDATA[ + const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"); + switch (this.type) { case "time": { const { hour, minute } = detail.value; const format = detail.format || "12"; - const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"); this.postMessageToPicker({ - name: "TimePickerInit", + name: "PickerInit", detail: { hour, minute, @@ -86,6 +113,20 @@ }); break; } + case "date": { + const { year, month, day } = detail.value; + this.postMessageToPicker({ + name: "PickerInit", + detail: { + year, + // Month value from input box starts from 1 instead of 0 + month: month == undefined ? undefined : month - 1, + day, + locale + } + }); + break; + } } ]]></body> </method> @@ -109,6 +150,10 @@ } break; } + case "date": { + this.sendPickerValueChanged(this.pickerState); + break; + } } ]]></body> </method> @@ -125,6 +170,17 @@ })); break; } + case "date": { + this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", { + detail: { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day + } + })); + break; + } } ]]></body> </method> @@ -152,11 +208,15 @@ } switch (aEvent.data.name) { - case "TimePickerPopupChanged": { + case "PickerPopupChanged": { this.pickerState = aEvent.data.detail; this.setInputBoxValue(); break; } + case "ClosePopup": { + this.closePicker(); + break; + } } ]]></body> </method> diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 208ab1931..059e151fc 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -98,7 +98,7 @@ function Spinner(props, context) { setState(newState) { const { spinner } = this.elements; const { value, items } = this.state; - const { value: newValue, items: newItems, isValueSet, isInvalid } = newState; + const { value: newValue, items: newItems, isValueSet, isInvalid, smoothScroll = true } = newState; if (this._isArrayDiff(newItems, items)) { this.state = Object.assign(this.state, newState); @@ -106,16 +106,18 @@ function Spinner(props, context) { this._scrollTo(newValue, true); } else if (newValue != value) { this.state = Object.assign(this.state, newState); - this._smoothScrollTo(newValue); - } - - if (isValueSet) { - if (isInvalid) { - this._removeSelection(); + if (smoothScroll) { + this._smoothScrollTo(newValue, true); } else { - this._updateSelection(); + this._scrollTo(newValue, true); } } + + if (isValueSet && !isInvalid) { + this._updateSelection(); + } else { + this._removeSelection(); + } }, /** @@ -266,11 +268,11 @@ function Spinner(props, context) { * Attach event listeners to the spinner and buttons. */ _attachEventListeners() { - const { spinner } = this.elements; + const { spinner, container } = this.elements; spinner.addEventListener("scroll", this, { passive: true }); - document.addEventListener("mouseup", this, { passive: true }); - document.addEventListener("mousedown", this); + container.addEventListener("mouseup", this, { passive: true }); + container.addEventListener("mousedown", this, { passive: true }); }, /** @@ -288,9 +290,6 @@ function Spinner(props, context) { break; } case "mousedown": { - // Use preventDefault to keep focus on input boxes - event.preventDefault(); - event.target.setCapture(); this.state.mouseState = { down: true, layerX: event.layerX, diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js index f438e9ec6..8c2fb89dd 100644 --- a/toolkit/content/widgets/timepicker.js +++ b/toolkit/content/widgets/timepicker.js @@ -206,7 +206,7 @@ function TimePicker(context) { // The panel is listening to window for postMessage event, so we // do postMessage to itself to send data to input boxes. window.postMessage({ - name: "TimePickerPopupChanged", + name: "PickerPopupChanged", detail: { hour, minute, @@ -218,6 +218,7 @@ function TimePicker(context) { }, _attachEventListeners() { window.addEventListener("message", this); + document.addEventListener("mousedown", this); }, /** @@ -231,6 +232,12 @@ function TimePicker(context) { this.handleMessage(event); break; } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + break; + } } }, @@ -241,11 +248,11 @@ function TimePicker(context) { */ handleMessage(event) { switch (event.data.name) { - case "TimePickerSetValue": { + case "PickerSetValue": { this.set(event.data.detail); break; } - case "TimePickerInit": { + case "PickerInit": { this.init(event.data.detail); break; } diff --git a/toolkit/modules/DateTimePickerHelper.jsm b/toolkit/modules/DateTimePickerHelper.jsm index 398687988..769ae0094 100644 --- a/toolkit/modules/DateTimePickerHelper.jsm +++ b/toolkit/modules/DateTimePickerHelper.jsm @@ -97,13 +97,10 @@ this.DateTimePickerHelper = { // Called when picker value has changed, notify input box about it. updateInputBoxValue: function(aEvent) { - // TODO: parse data based on input type. - const { hour, minute } = aEvent.detail; - debug("hour: " + hour + ", minute: " + minute); let browser = this.weakBrowser ? this.weakBrowser.get() : null; if (browser) { browser.messageManager.sendAsyncMessage( - "FormDateTime:PickerValueChanged", { hour, minute }); + "FormDateTime:PickerValueChanged", aEvent.detail); } }, @@ -141,7 +138,7 @@ this.DateTimePickerHelper = { this.picker.loadPicker(type, detail); // 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", rect.left, rect.top); + this.picker.openPopup(this._anchor, "after_start", 0, 0); this.addPickerListeners(); }, diff --git a/toolkit/themes/shared/datetimeinputpickers.css b/toolkit/themes/shared/datetimeinputpickers.css new file mode 100644 index 000000000..741f15281 --- /dev/null +++ b/toolkit/themes/shared/datetimeinputpickers.css @@ -0,0 +1,339 @@ +/* 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/. */ + +:root { + --font-size-default: 1.1rem; + --spinner-width: 3rem; + --spinner-margin-top-bottom: 0.4rem; + --spinner-item-height: 2.4rem; + --spinner-item-margin-bottom: 0.1rem; + --spinner-button-height: 1.2rem; + --colon-width: 2rem; + --day-period-spacing-width: 1rem; + --calendar-width: 23.1rem; + --date-picker-item-height: 2.4rem; + + --border: 0.1rem solid #D6D6D6; + --border-radius: 0.3rem; + --border-active-color: #B1B1B1; + + --font-color: #191919; + --fill-color: #EBEBEB; + + --selected-font-color: #FFFFFF; + --selected-fill-color: #0996F8; + + --button-font-color: #858585; + --button-font-color-hover: #4D4D4D; + --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; + + --disabled-opacity: 0.2; +} + +html { + font-size: 10px; +} + +body { + margin: 0; + color: var(--font-color); + font: message-box; + font-size: var(--font-size-default); +} + +button { + -moz-appearance: none; + background: none; + border: none; +} + +.nav { + display: flex; + width: var(--calendar-width); + height: 2.4rem; + margin-bottom: 0.8rem; + justify-content: space-between; +} + +.nav > button { + width: 3rem; + height: var(--date-picker-item-height); + background-color: var(--button-font-color); +} + +.nav > button:hover { + background-color: var(--button-font-color-hover); +} + +.nav > button.active { + background-color: var(--button-font-color-active); +} + +.nav > button.left { + background: url("chrome://global/skin/icons/calendar-arrows.svg#left") no-repeat 50% 50%; +} + +.nav > button.right { + background: url("chrome://global/skin/icons/calendar-arrows.svg#right") no-repeat 50% 50%; +} + +.month-year-container { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 3rem; + width: 17.1rem; + height: var(--date-picker-item-height); + z-index: 10; +} + +button.month-year { + font-size: 1.3rem; + border: var(--border); + border-radius: 0.3rem; + padding: 0.2rem 2.6rem 0.2rem 1.2rem; +} + +button.month-year:hover { + background: var(--fill-color); +} + +button.month-year.active { + border-color: var(--border-active-color); + background: var(--button-fill-color-active); +} + +button.month-year::after { + position: absolute; + content: ""; + width: 2.6rem; + height: 1.6rem; + background: url("chrome://global/skin/icons/spinner-arrows.svg#down") no-repeat 50% 50%; + background-color: var(--button-font-color); +} + +button.month-year.active::after { + background: url("chrome://global/skin/icons/spinner-arrows.svg#up") no-repeat 50% 50%; +} + +.month-year-view { + position: absolute; + z-index: 5; + padding-top: 3.2rem; + top: 0; + left: 0; + bottom: 0; + width: var(--calendar-width); + background: window; + opacity: 1; + transition: opacity 0.15s; +} + +.month-year-view.hidden { + visibility: hidden; + opacity: 0; +} + +.month-year-view > .spinner-container { + width: 5.5rem; + margin: 0 0.5rem; +} + +.month-year-view .spinner { + transform: scaleY(1); + transform-origin: top; + transition: transform 0.15s; +} + +.month-year-view.hidden .spinner { + transform: scaleY(0); + transition: none; +} + +.month-year-view .spinner > div { + transform: scaleY(1); + transition: transform 0.15s; +} + +.month-year-view.hidden .spinner > div { + transform: scaleY(2.5); + transition: none; +} + +.calendar-container { + cursor: default; + display: flex; + flex-direction: column; + width: var(--calendar-width); +} + +.week-header { + display: flex; +} + +.week-header > div { + color: var(--weekday-font-color); +} + +.week-header > div.weekend { + color: var(--weekend-font-color); +} + +.days-viewport { + height: 15rem; + overflow: hidden; + position: relative; +} + +.days-view { + position: absolute; + display: flex; + flex-wrap: wrap; + flex-direction: row; +} + +.week-header > div, +.days-view > div { + align-items: center; + display: flex; + height: var(--date-picker-item-height); + margin: 0.05rem 0.15rem; + position: relative; + justify-content: center; + width: 3rem; +} + +.days-view > div.outside { + color: var(--weekday-outside-font-color); +} + +.days-view > div.weekend { + color: var(--weekend-font-color); +} + +.days-view > div.weekend.outside { + color: var(--weekend-outside-font-color); +} + +#time-picker, +.month-year-view { + display: flex; + flex-direction: row; + justify-content: center; +} + +.spinner-container { + display: flex; + flex-direction: column; + width: var(--spinner-width); +} + +.spinner-container > button { + background-color: var(--button-font-color); + height: var(--spinner-button-height); +} + +.spinner-container > button:hover { + background-color: var(--button-font-color-hover); +} + +.spinner-container > button.active { + background-color: var(--button-font-color-active); +} + +.spinner-container > button.up { + background: url("chrome://global/skin/icons/spinner-arrows.svg#up") no-repeat 50% 50%; +} + +.spinner-container > button.down { + background: url("chrome://global/skin/icons/spinner-arrows.svg#down") no-repeat 50% 50%; +} + +.spinner-container.hide-buttons > button { + visibility: hidden; +} + +.spinner-container > .spinner { + position: relative; + width: 100%; + margin: var(--spinner-margin-top-bottom) 0; + cursor: default; + overflow-y: scroll; + scroll-snap-type: mandatory; + scroll-snap-points-y: repeat(100%); +} + +.spinner-container > .spinner > div { + box-sizing: border-box; + position: relative; + text-align: center; + padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0; + margin-bottom: var(--spinner-item-margin-bottom); + height: var(--spinner-item-height); + -moz-user-select: none; + scroll-snap-coordinate: 0 0; +} + +.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; +} + +.spinner-container > .spinner:not(.scrolling) > div.selection, +.calendar-container .days-view > div.selection { + color: var(--selected-font-color); +} + +.spinner-container > .spinner > div.selection::before, +.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, +.spinner-container > .spinner.scrolling > div.selection::before, +.spinner-container > .spinner.scrolling > div:hover::before { + display: none; +} + +.spinner-container > .spinner > div.disabled { + opacity: var(--disabled-opacity); +} + +.colon { + display: flex; + justify-content: center; + align-items: center; + width: var(--colon-width); + margin-bottom: 0.3rem; +} + +.spacer { + width: var(--day-period-spacing-width); +}
\ No newline at end of file diff --git a/toolkit/themes/shared/datetimepopup.css b/toolkit/themes/shared/datetimepopup.css new file mode 100644 index 000000000..52f6fc7a2 --- /dev/null +++ b/toolkit/themes/shared/datetimepopup.css @@ -0,0 +1,11 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +panel[type="arrow"][side="top"], +panel[type="arrow"][side="bottom"] { + margin-left: 0; + margin-right: 0; +} diff --git a/toolkit/themes/shared/icons/calendar-arrows.svg b/toolkit/themes/shared/icons/calendar-arrows.svg new file mode 100644 index 000000000..858676f55 --- /dev/null +++ b/toolkit/themes/shared/icons/calendar-arrows.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <style> + path:not(:target) { + display: none; + } + </style> + <path id="right" d="M4.8 14L3 12.3 8.5 7 3 1.7 4.8 0 12 7"/> + <path id="left" d="M9.2 0L11 1.7 5.5 7 11 12.3 9.2 14 2 7"/> +</svg> diff --git a/toolkit/themes/shared/icons/spinner-arrows.svg b/toolkit/themes/shared/icons/spinner-arrows.svg new file mode 100644 index 000000000..a8ba72d6b --- /dev/null +++ b/toolkit/themes/shared/icons/spinner-arrows.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6"> + <style> + path:not(:target) { + display: none; + } + </style> + <path id="down" d="M0 1l1-1 4 4 4-4 1 1-5 5"/> + <path id="up" d="M0 5l1 1 4-4 4 4 1-1-5-5"/> +</svg> diff --git a/toolkit/themes/shared/jar.inc.mn b/toolkit/themes/shared/jar.inc.mn index 9c3d86a40..bdfca2a05 100644 --- a/toolkit/themes/shared/jar.inc.mn +++ b/toolkit/themes/shared/jar.inc.mn @@ -21,12 +21,15 @@ toolkit.jar: skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css) skin/classic/global/appPicker.css (../../shared/appPicker.css) skin/classic/global/config.css (../../shared/config.css) - skin/classic/global/timepicker.css (../../shared/timepicker.css) + skin/classic/global/datetimeinputpickers.css (../../shared/datetimeinputpickers.css) + skin/classic/global/datetimepopup.css (../../shared/datetimepopup.css) + skin/classic/global/icons/calendar-arrows.svg (../../shared/icons/calendar-arrows.svg) skin/classic/global/icons/find-arrows.svg (../../shared/icons/find-arrows.svg) skin/classic/global/icons/info.svg (../../shared/incontent-icons/info.svg) skin/classic/global/icons/input-clear.svg (../../shared/icons/input-clear.svg) skin/classic/global/icons/loading.png (../../shared/icons/loading.png) skin/classic/global/icons/loading@2x.png (../../shared/icons/loading@2x.png) + skin/classic/global/icons/spinner-arrows.svg (../../shared/icons/spinner-arrows.svg) skin/classic/global/icons/warning.svg (../../shared/incontent-icons/warning.svg) skin/classic/global/icons/blocked.svg (../../shared/incontent-icons/blocked.svg) skin/classic/global/alerts/alert-common.css (../../shared/alert-common.css) diff --git a/toolkit/themes/shared/timepicker.css b/toolkit/themes/shared/timepicker.css deleted file mode 100644 index e8d081b30..000000000 --- a/toolkit/themes/shared/timepicker.css +++ /dev/null @@ -1,153 +0,0 @@ -/* 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/. */ - -:root { - --font-size-default: 1.1rem; - --spinner-width: 3rem; - --spinner-margin-top-bottom: 0.4rem; - --spinner-item-height: 2.4rem; - --spinner-item-margin-bottom: 0.1rem; - --spinner-button-height: 1.2rem; - --colon-width: 2rem; - --day-period-spacing-width: 1rem; - - --border: 0.1rem solid #D6D6D6; - --border-radius: 0.3rem; - - --font-color: #191919; - --fill-color: #EBEBEB; - - --selected-font-color: #FFFFFF; - --selected-fill-color: #0996F8; - - --button-font-color: #858585; - --button-font-color-hover: #4D4D4D; - --button-font-color-active: #191919; - - --disabled-opacity: 0.2; -} - -html { - font-size: 10px; -} - -body { - margin: 0; - color: var(--font-color); - font-size: var(--font-size-default); -} - -#time-picker { - display: flex; - flex-direction: row; - justify-content: space-around; -} - -.spinner-container { - font-family: sans-serif; - display: flex; - flex-direction: column; - width: var(--spinner-width); -} - -.spinner-container > button { - -moz-appearance: none; - border: none; - background: none; - background-color: var(--button-font-color); - height: var(--spinner-button-height); -} - -.spinner-container > button:hover { - background-color: var(--button-font-color-hover); -} - -.spinner-container > button.active { - background-color: var(--button-font-color-active); -} - -.spinner-container > button.up { - mask: url("chrome://global/skin/icons/find-arrows.svg#glyph-find-previous") no-repeat 50% 50%; -} - -.spinner-container > button.down { - mask: url("chrome://global/skin/icons/find-arrows.svg#glyph-find-next") no-repeat 50% 50%; -} - -.spinner-container.hide-buttons > button { - visibility: hidden; -} - -.spinner-container > .spinner { - position: relative; - width: 100%; - margin: var(--spinner-margin-top-bottom) 0; - cursor: default; - overflow-y: scroll; - scroll-snap-type: mandatory; - scroll-snap-points-y: repeat(100%); -} - -.spinner-container > .spinner > div { - box-sizing: border-box; - position: relative; - text-align: center; - padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0; - margin-bottom: var(--spinner-item-margin-bottom); - height: var(--spinner-item-height); - -moz-user-select: none; - scroll-snap-coordinate: 0 0; -} - -.spinner-container > .spinner > 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; -} - -.spinner-container > .spinner:not(.scrolling) > div.selection { - color: var(--selected-font-color); -} - -.spinner-container > .spinner > 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, -.spinner-container > .spinner.scrolling > div.selection::before, -.spinner-container > .spinner.scrolling > div:hover::before { - display: none; -} - -.spinner-container > .spinner > div.disabled { - opacity: var(--disabled-opacity); -} - -.colon { - display: flex; - justify-content: center; - align-items: center; - width: var(--colon-width); - margin-bottom: 0.3rem; -} - -.spacer { - width: var(--day-period-spacing-width); -}
\ No newline at end of file |