Bug 1288591 - Implement the layout for <input type=time>. r=mconley, r=dholbert, r=smaug

This commit is contained in:
Jessica Jong 2016-10-06 00:17:00 -04:00
Родитель b59026eb98
Коммит b3014cc00b
47 изменённых файлов: 2474 добавлений и 40 удалений

Просмотреть файл

@ -154,6 +154,12 @@
flip="none"
level="parent"/>
<panel id="DateTimePickerPanel"
hidden="true"
noautofocus="true"
consumeoutsideclicks="false"
level="parent"/>
<!-- for select dropdowns. The menupopup is what shows the list of options,
and the popuponly menulist makes things like the menuactive attributes
work correctly on the menupopup. ContentSelectDropdown expects the
@ -1056,7 +1062,8 @@
tabcontainer="tabbrowser-tabs"
contentcontextmenu="contentAreaContextMenu"
autocompletepopup="PopupAutoComplete"
selectmenulist="ContentSelectDropdown"/>
selectmenulist="ContentSelectDropdown"
datetimepicker="DateTimePickerPanel"/>
</vbox>
<vbox id="browser-border-end" hidden="true" layer="true"/>
</hbox>

Просмотреть файл

@ -25,7 +25,7 @@
<xul:vbox flex="1" class="browserContainer">
<xul:stack flex="1" class="browserStack" anonid="browserStack">
<xul:browser anonid="initialBrowser" type="content-primary" message="true" messagemanagergroup="browsers"
xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist"/>
xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist,datetimepicker"/>
</xul:stack>
</xul:vbox>
</xul:hbox>
@ -1886,6 +1886,10 @@
if (this.hasAttribute("selectmenulist"))
b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
if (this.hasAttribute("datetimepicker")) {
b.setAttribute("datetimepicker", this.getAttribute("datetimepicker"));
}
b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
if (aParams.relatedBrowser) {

Просмотреть файл

@ -33,6 +33,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-s
["ContentClick", "resource:///modules/ContentClick.jsm"],
["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm"],
["ContentSearch", "resource:///modules/ContentSearch.jsm"],
["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"],
["DirectoryLinksProvider", "resource:///modules/DirectoryLinksProvider.jsm"],
["Feeds", "resource:///modules/Feeds.jsm"],
["FileUtils", "resource://gre/modules/FileUtils.jsm"],
@ -1022,6 +1023,7 @@ BrowserGlue.prototype = {
CaptivePortalWatcher.init();
AutoCompletePopup.init();
DateTimePickerHelper.init();
this._firstWindowTelemetry(aWindow);
this._firstWindowLoaded();
@ -1053,6 +1055,7 @@ BrowserGlue.prototype = {
webrtcUI.uninit();
FormValidationHandler.uninit();
AutoCompletePopup.uninit();
DateTimePickerHelper.uninit();
if (AppConstants.NIGHTLY_BUILD) {
AddonWatcher.uninit();
}

Просмотреть файл

@ -272,6 +272,7 @@ GK_ATOM(dataType, "data-type")
GK_ATOM(dateTime, "date-time")
GK_ATOM(datasources, "datasources")
GK_ATOM(datetime, "datetime")
GK_ATOM(datetimebox, "datetimebox")
GK_ATOM(dblclick, "dblclick")
GK_ATOM(dd, "dd")
GK_ATOM(debug, "debug")
@ -1985,6 +1986,7 @@ GK_ATOM(colorControlFrame, "colorControlFrame")
GK_ATOM(columnSetFrame, "ColumnSetFrame")
GK_ATOM(comboboxControlFrame, "ComboboxControlFrame")
GK_ATOM(comboboxDisplayFrame, "ComboboxDisplayFrame")
GK_ATOM(dateTimeControlFrame, "DateTimeControlFrame")
GK_ATOM(deckFrame, "DeckFrame")
GK_ATOM(detailsFrame, "DetailsFrame")
GK_ATOM(fieldSetFrame, "FieldSetFrame")

Просмотреть файл

@ -53,6 +53,7 @@
#include "nsIIOService.h"
#include "nsDocument.h"
#include "nsAttrValueOrString.h"
#include "nsDateTimeControlFrame.h"
#include "nsPresState.h"
#include "nsIDOMEvent.h"
@ -2707,6 +2708,82 @@ HTMLInputElement::MozSetDirectory(const nsAString& aDirectoryPath,
SetFilesOrDirectories(array, true);
}
void HTMLInputElement::GetDateTimeInputBoxValue(DateTimeValue& aValue)
{
if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) {
return;
}
aValue = *mDateTimeInputBoxValue;
}
void
HTMLInputElement::UpdateDateTimeInputBox(const DateTimeValue& aValue)
{
if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
return;
}
nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
if (frame) {
frame->SetValueFromPicker(aValue);
}
}
void
HTMLInputElement::SetDateTimePickerState(bool aOpen)
{
if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
return;
}
nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
if (frame) {
frame->SetPickerState(aOpen);
}
}
void
HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue)
{
if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
return;
}
mDateTimeInputBoxValue = new DateTimeValue(aInitialValue);
nsContentUtils::DispatchChromeEvent(OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(this),
NS_LITERAL_STRING("MozOpenDateTimePicker"),
true, true);
}
void
HTMLInputElement::UpdateDateTimePicker(const DateTimeValue& aValue)
{
if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
return;
}
mDateTimeInputBoxValue = new DateTimeValue(aValue);
nsContentUtils::DispatchChromeEvent(OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(this),
NS_LITERAL_STRING("MozUpdateDateTimePicker"),
true, true);
}
void
HTMLInputElement::CloseDateTimePicker()
{
if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
return;
}
nsContentUtils::DispatchChromeEvent(OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(this),
NS_LITERAL_STRING("MozCloseDateTimePicker"),
true, true);
}
bool
HTMLInputElement::MozIsTextField(bool aExcludePassword)
{
@ -2733,6 +2810,28 @@ HTMLInputElement::GetOwnerNumberControl()
return nullptr;
}
HTMLInputElement*
HTMLInputElement::GetOwnerDateTimeControl()
{
if (IsInNativeAnonymousSubtree() &&
mType == NS_FORM_INPUT_TEXT &&
GetParent() &&
GetParent()->GetParent() &&
GetParent()->GetParent()->GetParent() &&
GetParent()->GetParent()->GetParent()->GetParent()) {
// Yes, this is very very deep.
HTMLInputElement* ownerDateTimeControl =
HTMLInputElement::FromContentOrNull(
GetParent()->GetParent()->GetParent()->GetParent());
if (ownerDateTimeControl &&
ownerDateTimeControl->mType == NS_FORM_INPUT_TIME) {
return ownerDateTimeControl;
}
}
return nullptr;
}
NS_IMETHODIMP
HTMLInputElement::MozIsTextField(bool aExcludePassword, bool* aResult)
{
@ -2740,6 +2839,19 @@ HTMLInputElement::MozIsTextField(bool aExcludePassword, bool* aResult)
return NS_OK;
}
void
HTMLInputElement::SetUserInput(const nsAString& aInput,
const mozilla::Maybe<nsIPrincipal*>& aPrincipal) {
MOZ_ASSERT(aPrincipal.isSome());
if (mType == NS_FORM_INPUT_FILE &&
!nsContentUtils::IsSystemPrincipal(aPrincipal.value())) {
return;
}
SetUserInput(aInput);
}
NS_IMETHODIMP
HTMLInputElement::SetUserInput(const nsAString& aValue)
{
@ -3164,6 +3276,12 @@ HTMLInputElement::SetValueInternal(const nsAString& aValue, uint32_t aFlags)
if (frame) {
frame->UpdateForValueChange();
}
} else if (mType == NS_FORM_INPUT_TIME &&
!IsExperimentalMobileType(mType)) {
nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
if (frame) {
frame->UpdateInputBoxValue();
}
}
if (!mParserCreating) {
OnValueChanged(/* aNotify = */ true,
@ -3466,6 +3584,15 @@ HTMLInputElement::Blur(ErrorResult& aError)
}
}
}
if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) {
nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
if (frame) {
frame->HandleBlurEvent();
return;
}
}
nsGenericHTMLElement::Blur(aError);
}
@ -3485,6 +3612,14 @@ HTMLInputElement::Focus(ErrorResult& aError)
}
}
if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) {
nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
if (frame) {
frame->HandleFocusEvent();
return;
}
}
if (mType != NS_FORM_INPUT_FILE) {
nsGenericHTMLElement::Focus(aError);
return;
@ -3788,7 +3923,7 @@ HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor)
// Experimental mobile types rely on the system UI to prevent users to not
// set invalid values but we have to be extra-careful. Especially if the
// option has been enabled on desktop.
if (IsExperimentalMobileType(mType) || IsDateTimeInputType(mType)) {
if (IsExperimentalMobileType(mType)) {
nsAutoString aValue;
GetValueInternal(aValue);
nsresult rv =
@ -3809,6 +3944,18 @@ HTMLInputElement::PreHandleEvent(EventChainPreVisitor& aVisitor)
}
}
if (mType == NS_FORM_INPUT_TIME &&
!IsExperimentalMobileType(mType) &&
aVisitor.mEvent->mMessage == eFocus &&
aVisitor.mEvent->mOriginalTarget == this) {
// If original target is this and not the anonymous text control, we should
// pass the focus to the anonymous text control.
nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
if (frame) {
frame->HandleFocusEvent();
}
}
if (mType == NS_FORM_INPUT_NUMBER && aVisitor.mEvent->IsTrusted()) {
if (mNumberControlSpinnerIsSpinning) {
// If the timer is running the user has depressed the mouse on one of the
@ -6682,6 +6829,11 @@ HTMLInputElement::AddStates(EventStates aStates)
HTMLInputElement* ownerNumberControl = GetOwnerNumberControl();
if (ownerNumberControl) {
ownerNumberControl->AddStates(focusStates);
} else {
HTMLInputElement* ownerDateTimeControl = GetOwnerDateTimeControl();
if (ownerDateTimeControl) {
ownerDateTimeControl->AddStates(focusStates);
}
}
}
}
@ -6698,6 +6850,11 @@ HTMLInputElement::RemoveStates(EventStates aStates)
HTMLInputElement* ownerNumberControl = GetOwnerNumberControl();
if (ownerNumberControl) {
ownerNumberControl->RemoveStates(focusStates);
} else {
HTMLInputElement* ownerDateTimeControl = GetOwnerDateTimeControl();
if (ownerDateTimeControl) {
ownerDateTimeControl->RemoveStates(focusStates);
}
}
}
}
@ -6875,12 +7032,14 @@ HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, int32_t*
#endif
if (mType == NS_FORM_INPUT_FILE ||
mType == NS_FORM_INPUT_NUMBER) {
mType == NS_FORM_INPUT_NUMBER ||
mType == NS_FORM_INPUT_TIME) {
if (aTabIndex) {
// We only want our native anonymous child to be tabable to, not ourself.
*aTabIndex = -1;
}
if (mType == NS_FORM_INPUT_NUMBER) {
if (mType == NS_FORM_INPUT_NUMBER ||
mType == NS_FORM_INPUT_TIME) {
*aIsFocusable = true;
} else {
*aIsFocusable = defaultFocusable;

Просмотреть файл

@ -772,7 +772,24 @@ public:
void MozSetFileArray(const Sequence<OwningNonNull<File>>& aFiles);
void MozSetDirectory(const nsAString& aDirectoryPath, ErrorResult& aRv);
/*
* The following functions are called from datetime picker to let input box
* know the current state of the picker or to update the input box on changes.
*/
void GetDateTimeInputBoxValue(DateTimeValue& aValue);
void UpdateDateTimeInputBox(const DateTimeValue& aValue);
void SetDateTimePickerState(bool aOpen);
/*
* The following functions are called from datetime input box XBL to control
* and update the picker.
*/
void OpenDateTimePicker(const DateTimeValue& aInitialValue);
void UpdateDateTimePicker(const DateTimeValue& aValue);
void CloseDateTimePicker();
HTMLInputElement* GetOwnerNumberControl();
HTMLInputElement* GetOwnerDateTimeControl();
void StartNumberControlSpinnerSpin();
enum SpinnerStopState {
@ -803,7 +820,8 @@ public:
nsIEditor* GetEditor();
// XPCOM SetUserInput() is OK
void SetUserInput(const nsAString& aInput,
const mozilla::Maybe<nsIPrincipal*>& aPrincipal);
// XPCOM GetPhonetic() is OK
@ -1471,6 +1489,12 @@ protected:
*/
Decimal mRangeThumbDragStartValue;
/**
* Current value in the input box, in DateTimeValue dictionary format, see
* HTMLInputElement.webidl for details.
*/
nsAutoPtr<DateTimeValue> mDateTimeInputBoxValue;
/**
* The selection properties cache for number controls. This is needed because
* the number controls don't recycle their text field, so the normal cache in
@ -1561,7 +1585,8 @@ private:
static bool MayFireChangeOnBlur(uint8_t aType) {
return IsSingleLineTextControl(false, aType) ||
aType == NS_FORM_INPUT_RANGE ||
aType == NS_FORM_INPUT_NUMBER;
aType == NS_FORM_INPUT_NUMBER ||
aType == NS_FORM_INPUT_TIME;
}
struct nsFilePickerFilter {

Просмотреть файл

@ -18,6 +18,7 @@ MOCHITEST_CHROME_MANIFESTS += [
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
XPIDL_SOURCES += [
'nsIDateTimeInputArea.idl',
'nsIFormSubmitObserver.idl',
'nsIHTMLMenu.idl',
'nsIImageDocument.idl',

Просмотреть файл

@ -0,0 +1,36 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
[scriptable, uuid(465c0cc3-24cb-48ce-af1a-b18402326b05)]
interface nsIDateTimeInputArea : nsISupports
{
/**
* Called from DOM/Layout when input element value has changed.
*/
void notifyInputElementValueChanged();
/**
* Called when date/time picker value has changed.
*/
void setValueFromPicker(in jsval value);
/**
* Called from DOM/Layout to set focus on inner text box.
*/
void focusInnerTextBox();
/**
* Called from DOM/Layout to blur inner text box.
*/
void blurInnerTextBox();
/**
* Set the current state of the picker, true if it's opened, false otherwise.
*/
void setPickerState(in boolean isOpen);
};

Просмотреть файл

@ -266,8 +266,11 @@ nsIFormControl::IsSingleLineTextControl(bool aExcludePassword, uint32_t aType)
aType == NS_FORM_INPUT_TEL ||
aType == NS_FORM_INPUT_URL ||
// TODO: those are temporary until bug 773205 is fixed.
aType == NS_FORM_INPUT_DATE ||
#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 ||
aType == NS_FORM_INPUT_MONTH ||
aType == NS_FORM_INPUT_WEEK ||
(!aExcludePassword && aType == NS_FORM_INPUT_PASSWORD);

Просмотреть файл

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html class="reftest-wait">
<!-- In this case we're using reftest-wait to make sure the test doesn't
get snapshotted before it's been focused. We're not testing
invalidation so we don't need to listen for MozReftestInvalidate.
-->
<head>
<script>
function focusHandler() {
setTimeout(function() {
document.documentElement.removeAttribute("class");
}, 0);
}
</script>
</head>
<body onload="document.getElementById('t').focus();">
<input type="time" id="t" onfocus="focusHandler();"
style="-moz-appearance: none;">
</body>
</html>

Просмотреть файл

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html class="reftest-wait">
<!-- In this case we're using reftest-wait to make sure the test doesn't
get snapshotted before it's been focused. We're not testing
invalidation so we don't need to listen for MozReftestInvalidate.
-->
<head>
<script>
function focusHandler() {
setTimeout(function() {
document.documentElement.removeAttribute("class");
}, 0);
}
</script>
</head>
<body>
<input type="time" autofocus onfocus="focusHandler();"
style="-moz-appearance: none;">
</body>
</html>

Просмотреть файл

@ -1,7 +1,8 @@
default-preferences pref(dom.forms.number,true)
default-preferences pref(dom.forms.number,true) pref(dom.forms.datetime,true)
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-load.html input-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-create.html input-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-number.html input-number-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-time.html input-time-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == button-load.html button-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == button-create.html button-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == textarea-load.html textarea-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop

Просмотреть файл

@ -35,6 +35,10 @@ skip-if = buildapp == 'mulet'
skip-if = android_version == '18' # Android, bug 1147974
[test_input_color_picker_update.html]
skip-if = android_version == '18' # Android, bug 1147974
[test_input_datetime_focus_blur.html]
skip-if = os == "android" || appname == "b2g"
[test_input_datetime_tabindex.html]
skip-if = os == "android" || appname == "b2g"
[test_input_defaultValue.html]
[test_input_email.html]
[test_input_event.html]
@ -64,6 +68,8 @@ skip-if = (toolkit == 'gonk' && debug) #debug-only failure; bug 926546
skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
[test_input_sanitization.html]
[test_input_textarea_set_value_no_scroll.html]
[test_input_time_key_events.html]
skip-if = os == "android" || appname == "b2g"
[test_input_types_pref.html]
[test_input_typing_sanitization.html]
skip-if = buildapp == 'mulet'

Просмотреть файл

@ -0,0 +1,58 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
-->
<head>
<title>Test focus/blur behaviour for &lt;input type='time'&gt;</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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=1288591">Mozilla Bug 1288591</a>
<p id="display"></p>
<div id="content">
<input id="input" type="time">
</div>
<pre id="test">
<script type="application/javascript">
/**
* Test for Bug 1288591.
* This test checks whether date/time input types' .focus()/.blur() works
* correctly. This test also checks when focusing on an date/time input element,
* the focus is redirected to the anonymous text control, but the
* document.activeElement still returns date/time input element.
**/
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
test();
SimpleTest.finish();
});
function test() {
let time = document.getElementById("input");
time.focus();
// The active element returns the input type=time.
let activeElement = document.activeElement;
is(activeElement, time, "activeElement should be the time element");
is(activeElement.localName, "input", "activeElement should be an input element");
is(activeElement.type, "time", "activeElement should be of type time");
// Use FocusManager to check that the actual focus is on the anonymous
// text control.
let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
.getService(SpecialPowers.Ci.nsIFocusManager);
let focusedElement = fm.focusedElement;
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");
}
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -0,0 +1,72 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
-->
<head>
<title>Test tabindex attribute for &lt;input type='time'&gt;</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"/>
</head>
<body>
<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="time1" type="time" tabindex="0">
<input id="time2" type="time" tabindex="-1">
<input id="time3" type="time" 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
* correctly.
**/
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
test();
SimpleTest.finish();
});
function test() {
let time1 = document.getElementById("time1");
let time2 = document.getElementById("time2");
let time3 = document.getElementById("time3");
time1.focus();
is(document.activeElement, time1,
"input element with tabindex=0 is focusable");
// Advance to time1 minute field
synthesizeKey("VK_TAB", {});
is(document.activeElement, time1,
"input element with tabindex=0 is tabbable");
// Advance to time1 AM/PM field
synthesizeKey("VK_TAB", {});
is(document.activeElement, time1,
"input element with tabindex=0 is tabbable");
// Advance to next element
synthesizeKey("VK_TAB", {});
is(document.activeElement, time3,
"input element with tabindex=-1 is not tabbable");
time2.focus();
is(document.activeElement, time2,
"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,
"element with tabindex changed to -1 should not be tabbable");
}
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -0,0 +1,197 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
-->
<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=1288591">Mozilla Bug 1288591</a>
<p id="display"></p>
<div id="content">
<input id="input" type="time">
</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 1030 and select AM.
keys: ["1030", "VK_DOWN"],
initialVal: "",
expectedVal: "10:30"
},
{
// Type 3 in the hour field will automatically advance to the minute field.
keys: ["330", "VK_DOWN"],
initialVal: "",
expectedVal: "03:30"
},
{
// Type 5 in the hour field will automatically advance to the minute field.
// Type 7 in the minute field will automatically advance to the AM/PM field.
keys: ["57", "VK_DOWN"],
initialVal: "",
expectedVal: "05:07"
},
{
// Advance to AM/PM field and change to PM.
keys: ["VK_TAB", "VK_TAB", "VK_DOWN"],
initialVal: "10:30",
expectedVal: "22:30"
},
{
// Right key should do the same thing as TAB key.
keys: ["VK_RIGHT", "VK_RIGHT", "VK_DOWN"],
initialVal: "10:30",
expectedVal: "22:30"
},
{
// Advance to minute field then back to hour field and decrement.
keys: ["VK_RIGHT", "VK_LEFT", "VK_DOWN"],
initialVal: "10:30",
expectedVal: "09:30"
},
{
// Focus starts on the first field, hour in this case, and increment.
keys: ["VK_UP"],
initialVal: "16:00",
expectedVal: "17:00"
},
{
// Advance to minute field and decrement.
keys: ["VK_TAB", "VK_DOWN"],
initialVal: "16:00",
expectedVal: "16:59"
},
{
// Advance to minute field and increment.
keys: ["VK_TAB", "VK_UP"],
initialVal: "16:59",
expectedVal: "16:00"
},
{
// PageUp on hour field increments hour by 3.
keys: ["VK_PAGE_UP"],
initialVal: "05:00",
expectedVal: "08:00"
},
{
// PageDown on hour field decrements hour by 3.
keys: ["VK_PAGE_DOWN"],
initialVal: "05:00",
expectedVal: "02:00"
},
{
// PageUp on minute field increments minute by 10.
keys: ["VK_TAB", "VK_PAGE_UP"],
initialVal: "14:00",
expectedVal: "14:10"
},
{
// PageDown on minute field decrements minute by 10.
keys: ["VK_TAB", "VK_PAGE_DOWN"],
initialVal: "14:00",
expectedVal: "14:50"
},
{
// Home key on hour field sets it to the minimum hour, which is 1 in 12-hour
// clock.
keys: ["VK_HOME"],
initialVal: "03:10",
expectedVal: "01:10"
},
{
// End key on hour field sets it to the maximum hour, which is 12 in 12-hour
// clock.
keys: ["VK_END"],
initialVal: "03:10",
expectedVal: "00:10"
},
{
// Home key on minute field sets it to the minimum minute, which is 0.
keys: ["VK_TAB", "VK_HOME"],
initialVal: "19:30",
expectedVal: "19:00"
},
{
// End key on minute field sets it to the minimum minute, which is 59.
keys: ["VK_TAB", "VK_END"],
initialVal: "19:30",
expectedVal: "19:59"
},
// Second field will show up when needed.
{
// PageUp on second field increments second by 10.
keys: ["VK_TAB", "VK_TAB", "VK_PAGE_UP"],
initialVal: "08:10:10",
expectedVal: "08:10:20"
},
{
// PageDown on second field increments second by 10.
keys: ["VK_TAB", "VK_TAB", "VK_PAGE_DOWN"],
initialVal: "08:10:10",
expectedVal: "08:10:00"
},
{
// Home key on second field sets it to the minimum second, which is 0.
keys: ["VK_TAB", "VK_TAB", "VK_HOME"],
initialVal: "16:00:30",
expectedVal: "16:00:00"
},
{
// End key on second field sets it to the minimum second, which is 59.
keys: ["VK_TAB", "VK_TAB", "VK_END"],
initialVal: "16:00:30",
expectedVal: "16:00:59"
},
];
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>

Просмотреть файл

@ -160,27 +160,6 @@ function runTest()
'1000-12-99',
]
},
{
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: [

Просмотреть файл

@ -196,7 +196,7 @@ partial interface HTMLInputElement {
// This is similar to set .value on nsIDOMInput/TextAreaElements, but handling
// of the value change is closer to the normal user input, so 'change' event
// for example will be dispatched when focusing out the element.
[ChromeOnly]
[Func="IsChromeOrXBL", NeedsSubjectPrincipal]
void setUserInput(DOMString input);
};
@ -234,3 +234,28 @@ partial interface HTMLInputElement {
[Pref="dom.webkitBlink.dirPicker.enabled", BinaryName="WebkitDirectoryAttr", SetterThrows]
attribute boolean webkitdirectory;
};
dictionary DateTimeValue {
long hour;
long minute;
};
partial interface HTMLInputElement {
[Pref="dom.forms.datetime", ChromeOnly]
DateTimeValue getDateTimeInputBoxValue();
[Pref="dom.forms.datetime", ChromeOnly]
void updateDateTimeInputBox(optional DateTimeValue value);
[Pref="dom.forms.datetime", ChromeOnly]
void setDateTimePickerState(boolean open);
[Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
void openDateTimePicker(optional DateTimeValue initialValue);
[Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
void updateDateTimePicker(optional DateTimeValue value);
[Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
void closeDateTimePicker();
};

Просмотреть файл

@ -861,7 +861,8 @@ nsXULElement::BindToTree(nsIDocument* aDocument,
// Note that add-ons may introduce bindings that cause this assertion to
// fire.
NS_ASSERTION(IsInVideoControls(this) ||
IsInFeedSubscribeLine(this),
IsInFeedSubscribeLine(this) ||
IsXULElement(nsGkAtoms::datetimebox),
"Unexpected XUL element in non-XUL doc");
}
}

Просмотреть файл

@ -3654,8 +3654,12 @@ nsCSSFrameConstructor::FindInputData(Element* aElement,
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),
// TODO: this is temporary until a frame is written: bug 888320
#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),
#else
SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewDateTimeControlFrame),
#endif
// TODO: this is temporary until a frame is written: bug 888320
SIMPLE_INT_CREATE(NS_FORM_INPUT_MONTH, NS_NewTextControlFrame),
// TODO: this is temporary until a frame is written: bug 888320

Просмотреть файл

@ -22,6 +22,7 @@ UNIFIED_SOURCES += [
'nsButtonFrameRenderer.cpp',
'nsColorControlFrame.cpp',
'nsComboboxControlFrame.cpp',
'nsDateTimeControlFrame.cpp',
'nsFieldSetFrame.cpp',
'nsFileControlFrame.cpp',
'nsFormControlFrame.cpp',

Просмотреть файл

@ -0,0 +1,414 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This frame type is used for input type=date, time, month, week, and
* datetime-local.
*/
#include "nsDateTimeControlFrame.h"
#include "nsContentUtils.h"
#include "nsFormControlFrame.h"
#include "nsGkAtoms.h"
#include "nsContentUtils.h"
#include "nsContentCreatorFunctions.h"
#include "nsContentList.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "nsNodeInfoManager.h"
#include "nsIDateTimeInputArea.h"
#include "nsIObserverService.h"
#include "nsIDOMHTMLInputElement.h"
#include "nsIDOMMutationEvent.h"
#include "jsapi.h"
#include "nsJSUtils.h"
#include "nsThreadUtils.h"
using namespace mozilla;
using namespace mozilla::dom;
nsIFrame*
NS_NewDateTimeControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
{
return new (aPresShell) nsDateTimeControlFrame(aContext);
}
NS_IMPL_FRAMEARENA_HELPERS(nsDateTimeControlFrame)
NS_QUERYFRAME_HEAD(nsDateTimeControlFrame)
NS_QUERYFRAME_ENTRY(nsDateTimeControlFrame)
NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
nsDateTimeControlFrame::nsDateTimeControlFrame(nsStyleContext* aContext)
: nsContainerFrame(aContext)
{
}
void
nsDateTimeControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
{
nsContentUtils::DestroyAnonymousContent(&mInputAreaContent);
nsContainerFrame::DestroyFrom(aDestructRoot);
}
void
nsDateTimeControlFrame::UpdateInputBoxValue()
{
nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
do_QueryInterface(mInputAreaContent);
if (inputAreaContent) {
inputAreaContent->NotifyInputElementValueChanged();
}
}
void
nsDateTimeControlFrame::SetValueFromPicker(const DateTimeValue& aValue)
{
nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
do_QueryInterface(mInputAreaContent);
if (inputAreaContent) {
AutoJSAPI api;
if (!api.Init(mContent->OwnerDoc()->GetScopeObject())) {
return;
}
JSObject* wrapper = mContent->GetWrapper();
if (!wrapper) {
return;
}
JSObject* scope = xpc::GetXBLScope(api.cx(), wrapper);
AutoJSAPI jsapi;
if (!scope || !jsapi.Init(scope)) {
return;
}
JS::Rooted<JS::Value> jsValue(jsapi.cx());
if (!ToJSValue(jsapi.cx(), aValue, &jsValue)) {
return;
}
inputAreaContent->SetValueFromPicker(jsValue);
}
}
void
nsDateTimeControlFrame::SetPickerState(bool aOpen)
{
nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
do_QueryInterface(mInputAreaContent);
if (inputAreaContent) {
inputAreaContent->SetPickerState(aOpen);
}
}
void
nsDateTimeControlFrame::HandleFocusEvent()
{
nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
do_QueryInterface(mInputAreaContent);
if (inputAreaContent) {
inputAreaContent->FocusInnerTextBox();
}
}
void
nsDateTimeControlFrame::HandleBlurEvent()
{
nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
do_QueryInterface(mInputAreaContent);
if (inputAreaContent) {
inputAreaContent->BlurInnerTextBox();
}
}
nscoord
nsDateTimeControlFrame::GetMinISize(nsRenderingContext* aRenderingContext)
{
nscoord result;
DISPLAY_MIN_WIDTH(this, result);
nsIFrame* kid = mFrames.FirstChild();
if (kid) { // display:none?
result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext,
kid,
nsLayoutUtils::MIN_ISIZE);
} else {
result = 0;
}
return result;
}
nscoord
nsDateTimeControlFrame::GetPrefISize(nsRenderingContext* aRenderingContext)
{
nscoord result;
DISPLAY_PREF_WIDTH(this, result);
nsIFrame* kid = mFrames.FirstChild();
if (kid) { // display:none?
result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext,
kid,
nsLayoutUtils::PREF_ISIZE);
} else {
result = 0;
}
return result;
}
void
nsDateTimeControlFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus)
{
MarkInReflow();
DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame");
DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
("enter nsDateTimeControlFrame::Reflow: availSize=%d,%d",
aReflowInput.AvailableWidth(),
aReflowInput.AvailableHeight()));
NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
const WritingMode myWM = aReflowInput.GetWritingMode();
// The ISize of our content box, which is the available ISize
// for our anonymous content:
const nscoord contentBoxISize = aReflowInput.ComputedISize();
nscoord contentBoxBSize = aReflowInput.ComputedBSize();
// Figure out our border-box sizes as well (by adding borderPadding to
// content-box sizes):
const nscoord borderBoxISize = contentBoxISize +
aReflowInput.ComputedLogicalBorderPadding().IStartEnd(myWM);
nscoord borderBoxBSize;
if (contentBoxBSize != NS_INTRINSICSIZE) {
borderBoxBSize = contentBoxBSize +
aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
} // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize.
nsIFrame* inputAreaFrame = mFrames.FirstChild();
if (!inputAreaFrame) { // display:none?
if (contentBoxBSize == NS_INTRINSICSIZE) {
contentBoxBSize = 0;
borderBoxBSize =
aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
}
} else {
NS_ASSERTION(inputAreaFrame->GetContent() == mInputAreaContent,
"What is this child doing here?");
ReflowOutput childDesiredSize(aReflowInput);
WritingMode wm = inputAreaFrame->GetWritingMode();
LogicalSize availSize = aReflowInput.ComputedSize(wm);
availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
ReflowInput childReflowOuput(aPresContext, aReflowInput,
inputAreaFrame, availSize);
// Convert input area margin into my own writing-mode (in case it differs):
LogicalMargin childMargin =
childReflowOuput.ComputedLogicalMargin().ConvertTo(myWM, wm);
// offsets of input area frame within this frame:
LogicalPoint
childOffset(myWM,
aReflowInput.ComputedLogicalBorderPadding().IStart(myWM) +
childMargin.IStart(myWM),
aReflowInput.ComputedLogicalBorderPadding().BStart(myWM) +
childMargin.BStart(myWM));
nsReflowStatus childStatus;
// We initially reflow the child with a dummy containerSize; positioning
// will be fixed later.
const nsSize dummyContainerSize;
ReflowChild(inputAreaFrame, aPresContext, childDesiredSize,
childReflowOuput, myWM, childOffset, dummyContainerSize, 0,
childStatus);
MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(childStatus),
"We gave our child unconstrained available block-size, "
"so it should be complete");
nscoord childMarginBoxBSize =
childDesiredSize.BSize(myWM) + childMargin.BStartEnd(myWM);
if (contentBoxBSize == NS_INTRINSICSIZE) {
// We are intrinsically sized -- we should shrinkwrap the input area's
// block-size:
contentBoxBSize = childMarginBoxBSize;
// Make sure we obey min/max-bsize in the case when we're doing intrinsic
// sizing (we get it for free when we have a non-intrinsic
// aReflowInput.ComputedBSize()). Note that we do this before
// adjusting for borderpadding, since ComputedMaxBSize and
// ComputedMinBSize are content heights.
contentBoxBSize =
NS_CSS_MINMAX(contentBoxBSize,
aReflowInput.ComputedMinBSize(),
aReflowInput.ComputedMaxBSize());
borderBoxBSize = contentBoxBSize +
aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
}
// Center child in block axis
nscoord extraSpace = contentBoxBSize - childMarginBoxBSize;
childOffset.B(myWM) += std::max(0, extraSpace / 2);
// Needed in FinishReflowChild, for logical-to-physical conversion:
nsSize borderBoxSize = LogicalSize(myWM, borderBoxISize, borderBoxBSize).
GetPhysicalSize(myWM);
// Place the child
FinishReflowChild(inputAreaFrame, aPresContext, childDesiredSize,
&childReflowOuput, myWM, childOffset, borderBoxSize, 0);
nsSize contentBoxSize =
LogicalSize(myWM, contentBoxISize, contentBoxBSize).
GetPhysicalSize(myWM);
aDesiredSize.SetBlockStartAscent(
childDesiredSize.BlockStartAscent() +
inputAreaFrame->BStart(aReflowInput.GetWritingMode(),
contentBoxSize));
}
LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize);
aDesiredSize.SetSize(myWM, logicalDesiredSize);
aDesiredSize.SetOverflowAreasToDesiredBounds();
if (inputAreaFrame) {
ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inputAreaFrame);
}
FinishAndStoreOverflow(&aDesiredSize);
aStatus = NS_FRAME_COMPLETE;
NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
("exit nsDateTimeControlFrame::Reflow: size=%d,%d",
aDesiredSize.Width(), aDesiredSize.Height()));
NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
}
nsresult
nsDateTimeControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
{
// Set up "datetimebox" XUL element which will be XBL-bound to the
// actual controls.
nsNodeInfoManager* nodeInfoManager =
mContent->GetComposedDoc()->NodeInfoManager();
RefPtr<NodeInfo> nodeInfo =
nodeInfoManager->GetNodeInfo(nsGkAtoms::datetimebox, nullptr,
kNameSpaceID_XUL, nsIDOMNode::ELEMENT_NODE);
NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
NS_TrustedNewXULElement(getter_AddRefs(mInputAreaContent), nodeInfo.forget());
aElements.AppendElement(mInputAreaContent);
// Propogate our tabindex.
nsAutoString tabIndexStr;
if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, tabIndexStr)) {
mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex,
tabIndexStr, false);
}
// Propagate our readonly state.
nsAutoString readonly;
if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly)) {
mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly,
false);
}
SyncDisabledState();
return NS_OK;
}
void
nsDateTimeControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
uint32_t aFilter)
{
if (mInputAreaContent) {
aElements.AppendElement(mInputAreaContent);
}
}
void
nsDateTimeControlFrame::SyncDisabledState()
{
EventStates eventStates = mContent->AsElement()->State();
if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
EmptyString(), true);
} else {
mInputAreaContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
}
}
nsresult
nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID,
nsIAtom* aAttribute,
int32_t aModType)
{
NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
// nsGkAtoms::disabled is handled by SyncDisabledState
if (aNameSpaceID == kNameSpaceID_None) {
if (aAttribute == nsGkAtoms::value ||
aAttribute == nsGkAtoms::readonly ||
aAttribute == nsGkAtoms::tabindex) {
MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast");
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 (aAttribute == nsGkAtoms::value) {
nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
do_QueryInterface(mInputAreaContent);
if (inputAreaContent) {
nsContentUtils::AddScriptRunner(NewRunnableMethod(inputAreaContent,
&nsIDateTimeInputArea::NotifyInputElementValueChanged));
}
} else {
if (aModType == nsIDOMMutationEvent::REMOVAL) {
mInputAreaContent->UnsetAttr(aNameSpaceID, aAttribute, true);
} else {
MOZ_ASSERT(aModType == nsIDOMMutationEvent::ADDITION ||
aModType == nsIDOMMutationEvent::MODIFICATION);
nsAutoString value;
mContent->GetAttr(aNameSpaceID, aAttribute, value);
mInputAreaContent->SetAttr(aNameSpaceID, aAttribute, value, true);
}
}
}
}
}
return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute,
aModType);
}
void
nsDateTimeControlFrame::ContentStatesChanged(EventStates aStates)
{
if (aStates.HasState(NS_EVENT_STATE_DISABLED)) {
nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this));
}
}
nsIAtom*
nsDateTimeControlFrame::GetType() const
{
return nsGkAtoms::dateTimeControlFrame;
}

Просмотреть файл

@ -0,0 +1,119 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This frame type is used for input type=date, time, month, week, and
* datetime-local.
*
* NOTE: some of the above-mentioned input types are still to-be-implemented.
* See nsCSSFrameConstructor::FindInputData, as well as bug 1286182 (date),
* bug 1306215 (month), bug 1306216 (week) and bug 1306217 (datetime-local).
*/
#ifndef nsDateTimeControlFrame_h__
#define nsDateTimeControlFrame_h__
#include "mozilla/Attributes.h"
#include "nsContainerFrame.h"
#include "nsIAnonymousContentCreator.h"
#include "nsCOMPtr.h"
namespace mozilla {
namespace dom {
struct DateTimeValue;
} // namespace dom
} // namespace mozilla
class nsDateTimeControlFrame final : public nsContainerFrame,
public nsIAnonymousContentCreator
{
typedef mozilla::dom::DateTimeValue DateTimeValue;
explicit nsDateTimeControlFrame(nsStyleContext* aContext);
public:
friend nsIFrame* NS_NewDateTimeControlFrame(nsIPresShell* aPresShell,
nsStyleContext* aContext);
void ContentStatesChanged(mozilla::EventStates aStates) override;
void DestroyFrom(nsIFrame* aDestructRoot) override;
NS_DECL_QUERYFRAME_TARGET(nsDateTimeControlFrame)
NS_DECL_QUERYFRAME
NS_DECL_FRAMEARENA_HELPERS
#ifdef DEBUG_FRAME_DUMP
nsresult GetFrameName(nsAString& aResult) const override {
return MakeFrameName(NS_LITERAL_STRING("DateTimeControl"), aResult);
}
#endif
nsIAtom* GetType() const override;
bool IsFrameOfType(uint32_t aFlags) const override
{
return nsContainerFrame::IsFrameOfType(aFlags &
~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock));
}
// Reflow
nscoord GetMinISize(nsRenderingContext* aRenderingContext) override;
nscoord GetPrefISize(nsRenderingContext* aRenderingContext) override;
void Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowState,
nsReflowStatus& aStatus) override;
// nsIAnonymousContentCreator
nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
uint32_t aFilter) override;
nsresult AttributeChanged(int32_t aNameSpaceID, nsIAtom* aAttribute,
int32_t aModType) override;
void UpdateInputBoxValue();
void SetValueFromPicker(const DateTimeValue& aValue);
void HandleFocusEvent();
void HandleBlurEvent();
void SetPickerState(bool aOpen);
private:
class SyncDisabledStateEvent;
friend class SyncDisabledStateEvent;
class SyncDisabledStateEvent : public mozilla::Runnable
{
public:
explicit SyncDisabledStateEvent(nsDateTimeControlFrame* aFrame)
: mFrame(aFrame)
{}
NS_IMETHOD Run() override
{
nsDateTimeControlFrame* frame =
static_cast<nsDateTimeControlFrame*>(mFrame.GetFrame());
NS_ENSURE_STATE(frame);
frame->SyncDisabledState();
return NS_OK;
}
private:
nsWeakFrame mFrame;
};
/**
* Sync the disabled state of the anonymous children up with our content's.
*/
void SyncDisabledState();
// Anonymous child which is bound via XBL to an element that wraps the input
// area and reset button.
nsCOMPtr<nsIContent> mInputAreaContent;
};
#endif // nsDateTimeControlFrame_h__

Просмотреть файл

@ -19,6 +19,7 @@ FRAME_ID(nsComboboxControlFrame)
FRAME_ID(nsComboboxDisplayFrame)
FRAME_ID(nsContainerFrame)
FRAME_ID(nsContinuingTextFrame)
FRAME_ID(nsDateTimeControlFrame)
FRAME_ID(nsDeckFrame)
FRAME_ID(nsDocElementBoxFrame)
FRAME_ID(nsFieldSetFrame)

Просмотреть файл

@ -171,6 +171,8 @@ nsIFrame*
NS_NewRangeFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
nsIFrame*
NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
nsIFrame*
NS_NewDateTimeControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
nsBlockFrame*
NS_NewDetailsFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="checkbox" style="-moz-appearance:none;">
</body>
</html>

Просмотреть файл

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html class="reftest-wait">
<!-- Test: when switching to another type, the input element should look
like that type (not like an input time element) -->
<script type="text/javascript">
function setToCheckbox()
{
document.getElementById("i").type = "checkbox";
document.documentElement.className = "";
}
document.addEventListener("MozReftestInvalidate", setToCheckbox);
</script>
<body>
<input type="time" id="i" style="-moz-appearance:none;">
</body>
</html>

Просмотреть файл

@ -0,0 +1,13 @@
default-preferences pref(dom.forms.datetime,true)
# not valid on Android/B2G where type=time looks like type=text
skip-if(Android||B2G||Mulet) != time-simple-unthemed.html time-simple-unthemed-ref.html
skip-if(Android||B2G||Mulet) != time-large-font.html time-basic.html
skip-if(Android||B2G||Mulet) != time-width-height.html time-basic.html
skip-if(Android||B2G||Mulet) != time-border.html time-basic.html
# only valid on Android/B2G where type=number looks the same as type=text
skip-if(!Android&&!B2G&&!Mulet) == time-simple-unthemed.html time-simple-unthemed-ref.html
# type change
skip-if(Android||B2G||Mulet) == to-time-from-other-type-unthemed.html time-simple-unthemed.html
skip-if(Android||B2G||Mulet) == from-time-to-other-type-unthemed.html from-time-to-other-type-unthemed-ref.html

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="time" value="12:30">
</body>
</html>

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="time" value="12:30" style="border:10px solid blue">
</body>
</html>

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="time" value="12:30" style="font-size: 32px;">
</body>
</html>

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="text" style="-moz-appearance:none;">
</body>
</html>

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="time" style="-moz-appearance:none;">
</body>
</html>

Просмотреть файл

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<input type="time" style="width:200px; height:50px">
</body>
</html>

Просмотреть файл

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html class="reftest-wait">
<!-- Test: input element changed to time state doesn't look like checkbox state -->
<script type="text/javascript">
function setToTime()
{
document.getElementById("i").type = "time";
document.documentElement.className = "";
}
document.addEventListener("MozReftestInvalidate", setToTime);
</script>
<body>
<input type="checkbox" id="i" style="-moz-appearance:none;">
</body>
</html>

Просмотреть файл

@ -11,3 +11,4 @@ include text/reftest.list
include percentage/reftest.list
include hidden/reftest.list
include color/reftest.list
include datetime/reftest.list

Просмотреть файл

@ -767,6 +767,13 @@ video > .caption-box {
overflow: hidden;
}
/* datetime elements */
input[type="time"] > xul|datetimebox {
display: flex;
-moz-binding: url("chrome://global/content/bindings/datetimebox.xml#time-input");
}
/* details & summary */
/* Need to revert Bug 1259889 Part 2 when removing details preference. */
@supports -moz-bool-pref("dom.details_element.enabled") {

Просмотреть файл

@ -167,7 +167,7 @@ function setupFormHistory(aCallback) {
{ op : "add", fieldname : "field9", value : "value" },
{ op : "add", fieldname : "field10", value : "42" },
{ op : "add", fieldname : "field11", value : "2010-10-10" },
{ op : "add", fieldname : "field12", value : "21:21" },
{ 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
{ op : "add", fieldname : "field15", value : "2016-08" },
@ -913,15 +913,13 @@ function runTest() {
input = $_(15, "field12");
restoreForm();
expectPopup();
doKey("down");
waitForMenuChange(0);
break;
case 406:
checkMenuEntries(["21:21"]);
doKey("down");
doKey("return");
checkForm("21:21");
checkMenuEntries([]); // type=time with it's own control frame does not
// have a drop down menu for now
checkForm("");
input = $_(16, "field13");
restoreForm();

Просмотреть файл

@ -1509,3 +1509,151 @@ let AutoCompletePopup = {
}
AutoCompletePopup.init();
/**
* DateTimePickerListener is the communication channel between the input box
* (content) for date/time input types and its picker (chrome).
*/
let DateTimePickerListener = {
/**
* On init, just listen for the event to open the picker, once the picker is
* opened, we'll listen for update and close events.
*/
init: function() {
addEventListener("MozOpenDateTimePicker", this);
this._inputElement = null;
addEventListener("unload", () => {
this.uninit();
});
},
uninit: function() {
removeEventListener("MozOpenDateTimePicker", this);
this._inputElement = null;
},
/**
* Cleanup function called when picker is closed.
*/
close: function() {
this.removeListeners();
this._inputElement.setDateTimePickerState(false);
this._inputElement = null;
},
/**
* Called after picker is opened to start listening for input box update
* events.
*/
addListeners: function() {
addEventListener("MozUpdateDateTimePicker", this);
addEventListener("MozCloseDateTimePicker", this);
addEventListener("pagehide", this);
addMessageListener("FormDateTime:PickerValueChanged", this);
addMessageListener("FormDateTime:PickerClosed", this);
},
/**
* Stop listeneing for events when picker is closed.
*/
removeListeners: function() {
removeEventListener("MozUpdateDateTimePicker", this);
removeEventListener("MozCloseDateTimePicker", this);
removeEventListener("pagehide", this);
removeMessageListener("FormDateTime:PickerValueChanged", this);
removeMessageListener("FormDateTime:PickerClosed", this);
},
/**
* Helper function that returns the CSS direction property of the element.
*/
getComputedDirection: function(aElement) {
return aElement.ownerDocument.defaultView.getComputedStyle(aElement)
.getPropertyValue("direction");
},
/**
* Helper function that returns the rect of the element, which is the position
* in "screen" coordinates.
*/
getBoundingContentRect: function(aElement) {
return BrowserUtils.getElementBoundingScreenRect(aElement);
},
/**
* nsIMessageListener.
*/
receiveMessage: function(aMessage) {
switch (aMessage.name) {
case "FormDateTime:PickerClosed": {
this.close();
break;
}
case "FormDateTime:PickerValueChanged": {
this._inputElement.updateDateTimeInputBox(aMessage.data);
break;
}
default:
break;
}
},
/**
* nsIDOMEventListener, for chrome events sent by the input element and other
* DOM events.
*/
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "MozOpenDateTimePicker": {
if (!(aEvent.originalTarget instanceof content.HTMLInputElement)) {
return;
}
this._inputElement = aEvent.originalTarget;
this._inputElement.setDateTimePickerState(true);
this.addListeners();
let value = this._inputElement.getDateTimeInputBoxValue();
sendAsyncMessage("FormDateTime:OpenPicker", {
rect: this.getBoundingContentRect(this._inputElement),
dir: this.getComputedDirection(this._inputElement),
type: this._inputElement.type,
detail: {
// Pass partial value if it's available, otherwise pass input
// element's value.
value: Object.keys(value).length > 0 ? value
: this._inputElement.value,
step: this._inputElement.step,
min: this._inputElement.min,
max: this._inputElement.max,
},
});
break;
}
case "MozUpdateDateTimePicker": {
let value = this._inputElement.getDateTimeInputBoxValue();
sendAsyncMessage("FormDateTime:UpdatePicker", { value });
break;
}
case "MozCloseDateTimePicker": {
sendAsyncMessage("FormDateTime:ClosePicker");
this.close();
break;
}
case "pagehide": {
if (this._inputElement &&
this._inputElement.ownerDocument == aEvent.target) {
sendAsyncMessage("FormDateTime:ClosePicker");
this.close();
}
break;
}
default:
break;
}
},
}
DateTimePickerListener.init();

Просмотреть файл

@ -71,6 +71,8 @@ toolkit.jar:
content/global/bindings/checkbox.xml (widgets/checkbox.xml)
content/global/bindings/colorpicker.xml (widgets/colorpicker.xml)
content/global/bindings/datetimepicker.xml (widgets/datetimepicker.xml)
content/global/bindings/datetimebox.xml (widgets/datetimebox.xml)
content/global/bindings/datetimebox.css (widgets/datetimebox.css)
* content/global/bindings/dialog.xml (widgets/dialog.xml)
content/global/bindings/editor.xml (widgets/editor.xml)
content/global/bindings/expander.xml (widgets/expander.xml)

Просмотреть файл

@ -281,6 +281,10 @@
onget="return document.getElementById(this.getAttribute('autocompletepopup'))"
readonly="true"/>
<property name="dateTimePicker"
onget="return document.getElementById(this.getAttribute('datetimepicker'))"
readonly="true"/>
<property name="docShellIsActive">
<getter>
<![CDATA[

Просмотреть файл

@ -0,0 +1,44 @@
/* 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.w3.org/1999/xhtml");
@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
.datetime-input-box-wrapper {
-moz-appearance: none;
display: inline-flex;
cursor: default;
background-color: inherit;
color: inherit;
}
.datetime-input {
-moz-appearance: none;
text-align: center;
padding: 0;
border: 0;
margin: 0;
ime-mode: disabled;
}
.datetime-separator {
margin: 0 !important;
}
.datetime-input[readonly],
.datetime-input[disabled] {
color: GrayText;
-moz-user-select: none;
}
.datetime-reset-button {
background-image: url(chrome://global/skin/icons/input-clear.svg);
background-repeat: no-repeat;
background-size: 12px, 12px;
border: none;
height: 12px;
width: 12px;
align-self: center;
justify-content: flex-end;
}

Просмотреть файл

@ -0,0 +1,806 @@
<?xml version="1.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/. -->
<bindings id="datetimeboxBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="time-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 1301312 - localization for input type=time input.
this.mHour12 = true;
this.mAMIndicator = "AM";
this.mPMIndicator = "PM";
this.mPlaceHolder = "--";
this.mSeparatorText = ":";
this.mMillisecSeparatorText = ".";
this.mMaxLength = 2;
this.mMillisecMaxLength = 3;
this.mDefaultStep = 60 * 1000; // in milliseconds
this.mMinHourInHour12 = 1;
this.mMaxHourInHour12 = 12;
this.mMinMinute = 0;
this.mMaxMinute = 59;
this.mMinSecond = 0;
this.mMaxSecond = 59;
this.mMinMillisecond = 0;
this.mMaxMillisecond = 999;
this.mHourPageUpDownInterval = 3;
this.mMinSecPageUpDownInterval = 10;
this.mHourField =
document.getAnonymousElementByAttribute(this, "anonid", "input-one");
this.mHourField.setAttribute("typeBuffer", "");
this.mMinuteField =
document.getAnonymousElementByAttribute(this, "anonid", "input-two");
this.mMinuteField.setAttribute("typeBuffer", "");
this.mDayPeriodField =
document.getAnonymousElementByAttribute(this, "anonid", "input-three");
this.mDayPeriodField.classList.remove("numeric");
this.mHourField.placeholder = this.mPlaceHolder;
this.mMinuteField.placeholder = this.mPlaceHolder;
this.mDayPeriodField.placeholder = this.mPlaceHolder;
this.mHourField.setAttribute("min", this.mMinHourInHour12);
this.mHourField.setAttribute("max", this.mMaxHourInHour12);
this.mMinuteField.setAttribute("min", this.mMinMinute);
this.mMinuteField.setAttribute("max", this.mMaxMinute);
this.mMinuteSeparator =
document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
this.mMinuteSeparator.textContent = this.mSeparatorText;
this.mSpaceSeparator =
document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
// space between time and am/pm field
this.mSpaceSeparator.textContent = " ";
this.mSecondSeparator = null;
this.mSecondField = null;
this.mMillisecSeparator = null;
this.mMillisecField = null;
if (this.mInputElement.value) {
this.setFieldsFromInputValue();
}
]]>
</constructor>
<method name="insertSeparator">
<parameter name="aSeparatorText"/>
<body>
<![CDATA[
let container = this.mHourField.parentNode;
const HTML_NS = "http://www.w3.org/1999/xhtml";
let separator = document.createElementNS(HTML_NS, "span");
separator.textContent = aSeparatorText;
separator.setAttribute("class", "datetime-separator");
container.insertBefore(separator, this.mSpaceSeparator);
return separator;
]]>
</body>
</method>
<method name="insertAdditionalField">
<parameter name="aPlaceHolder"/>
<parameter name="aMin"/>
<parameter name="aMax"/>
<parameter name="aSize"/>
<parameter name="aMaxLength"/>
<body>
<![CDATA[
let container = this.mHourField.parentNode;
const HTML_NS = "http://www.w3.org/1999/xhtml";
let field = document.createElementNS(HTML_NS, "input");
field.classList.add("textbox-input", "datetime-input", "numeric");
field.setAttribute("size", aSize);
field.setAttribute("maxlength", aMaxLength);
field.setAttribute("min", aMin);
field.setAttribute("max", aMax);
field.setAttribute("typeBuffer", "");
field.disabled = this.mInputElement.disabled;
field.readOnly = this.mInputElement.readOnly;
field.tabIndex = this.mInputElement.tabIndex;
field.placeholder = aPlaceHolder;
container.insertBefore(field, this.mSpaceSeparator);
return field;
]]>
</body>
</method>
<method name="setFieldsFromInputValue">
<body>
<![CDATA[
let value = this.mInputElement.value;
if (!value) {
this.clearInputFields(true);
return;
}
this.log("setFieldsFromInputValue: " + value);
let [hour, minute, second] = value.split(':');
this.setFieldValue(this.mHourField, hour);
this.setFieldValue(this.mMinuteField, minute);
if (this.mHour12) {
this.mDayPeriodField.value = (hour >= this.mMaxHourInHour12) ?
this.mPMIndicator : this.mAMIndicator;
}
if (!this.isEmpty(second)) {
let index = second.indexOf(".");
let millisecond;
if (index != -1) {
millisecond = second.substring(index + 1);
second = second.substring(0, index);
}
if (!this.mSecondField) {
this.mSecondSeparator = this.insertSeparator(this.mSeparatorText);
this.mSecondField = this.insertAdditionalField(this.mPlaceHolder,
this.mMinSecond, this.mMaxSecond, this.mMaxLength,
this.mMaxLength);
}
this.setFieldValue(this.mSecondField, second);
if (!this.isEmpty(millisecond)) {
if (!this.mMillisecField) {
this.mMillisecSeparator = this.insertSeparator(
this.mMillisecSeparatorText);
this.mMillisecField = this.insertAdditionalField(
this.mPlaceHolder, this.mMinMillisecond, this.mMaxMillisecond,
this.mMillisecMaxLength, this.mMillisecMaxLength);
}
this.setFieldValue(this.mMillisecField, millisecond);
} else if (this.mMillisecField) {
this.mMillisecField.remove();
this.mMillisecField = null;
this.mMillisecSeparator.remove();
this.mMillisecSeparator = null;
}
} else {
if (this.mSecondField) {
this.mSecondField.remove();
this.mSecondField = null;
this.mSecondSeparator.remove();
this.mSecondSeparator = null;
}
if (this.mMillisecField) {
this.mMillisecField.remove();
this.mMillisecField = null;
this.mMillisecSeparator.remove();
this.mMillisecSeparator = null;
}
}
this.notifyPicker();
]]>
</body>
</method>
<method name="setInputValueFromFields">
<body>
<![CDATA[
if (this.isEmpty(this.mHourField.value) ||
this.isEmpty(this.mMinuteField.value) ||
(this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) ||
(this.mSecondField && this.isEmpty(this.mSecondField.value))) {
// We still need to notify picker in case any of the field has
// changed. If we can set input element value, then notifyPicker
// will be called in setFieldsFromInputValue().
this.notifyPicker();
return;
}
let hour = Number(this.mHourField.value);
if (this.mHour12) {
let dayPeriod = this.mDayPeriodField.value;
if (dayPeriod == this.mPMIndicator &&
hour < this.mMaxHourInHour12) {
hour += this.mMaxHourInHour12;
} else if (dayPeriod == this.mAMIndicator &&
hour == this.mMaxHourInHour12) {
hour = 0;
}
}
hour = (hour < 10) ? ("0" + hour) : hour;
let time = hour + ":" + this.mMinuteField.value;
if (this.mSecondField) {
time += ":" + this.mSecondField.value;
}
if (this.mMillisecField) {
time += "." + this.mMillisecField.value;
}
this.log("setInputValueFromFields: " + time);
this.mInputElement.setUserInput(time);
]]>
</body>
</method>
<method name="setFieldsFromPicker">
<parameter name="aValue"/>
<body>
<![CDATA[
let hour = aValue.hour;
let minute = aValue.minute;
this.log("setFieldsFromPicker: " + hour + ":" + minute);
if (!this.isEmpty(hour)) {
this.setFieldValue(this.mHourField, hour);
if (this.mHour12) {
this.mDayPeriodField.value =
(hour >= this.mMaxHourInHour12) ? this.mPMIndicator
: this.mAMIndicator;
}
}
if (!this.isEmpty(minute)) {
this.setFieldValue(this.mMinuteField, minute);
}
]]>
</body>
</method>
<method name="clearInputFields">
<parameter name="aFromInputElement"/>
<body>
<![CDATA[
this.log("clearInputFields");
if (this.isDisabled() || this.isReadonly()) {
return;
}
if (this.mHourField && !this.mHourField.disabled &&
!this.mHourField.readOnly) {
this.mHourField.value = "";
}
if (this.mMinuteField && !this.mMinuteField.disabled &&
!this.mMinuteField.readOnly) {
this.mMinuteField.value = "";
}
if (this.mSecondField && !this.mSecondField.disabled &&
!this.mSecondField.readOnly) {
this.mSecondField.value = "";
}
if (this.mMillisecField && !this.mMillisecField.disabled &&
!this.mMillisecField.readOnly) {
this.mMillisecField.value = "";
}
if (this.mDayPeriodField && !this.mDayPeriodField.disabled &&
!this.mDayPeriodField.readOnly) {
this.mDayPeriodField.value = "";
}
if (!aFromInputElement) {
this.mInputElement.setUserInput("");
}
]]>
</body>
</method>
<method name="incrementFieldValue">
<parameter name="aTargetField"/>
<parameter name="aTimes"/>
<body>
<![CDATA[
let value;
// Use current time if field is empty.
if (this.isEmpty(aTargetField.value)) {
let now = new Date();
if (aTargetField == this.mHourField) {
value = now.getHours() % this.mMaxHourInHour12 ||
this.mMaxHourInHour12;
} else if (aTargetField == this.mMinuteField) {
value = now.getMinutes();
} else if (aTargetField == this.mSecondField) {
value = now.getSeconds();
} else if (aTargetField == this.mMillisecondsField) {
value = now.getMilliseconds();
} else {
this.log("Field not supported in incrementFieldValue.");
return;
}
} else {
value = Number(aTargetField.value);
}
let min = aTargetField.getAttribute("min");
let max = aTargetField.getAttribute("max");
value += 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;
if (this.mDayPeriodField &&
targetField == this.mDayPeriodField) {
// Home/End key does nothing on AM/PM field.
if (key == "Home" || key == "End") {
return;
}
this.mDayPeriodField.value =
this.mDayPeriodField.value == this.mAMIndicator ?
this.mPMIndicator : this.mAMIndicator;
this.mDayPeriodField.select();
this.setInputValueFromFields();
return;
}
switch (key) {
case "ArrowUp":
this.incrementFieldValue(targetField, 1);
break;
case "ArrowDown":
this.incrementFieldValue(targetField, -1);
break;
case "PageUp":
this.incrementFieldValue(targetField,
targetField == this.mHourField ? this.mHourPageUpDownInterval
: this.mMinSecPageUpDownInterval);
break;
case "PageDown":
this.incrementFieldValue(targetField,
targetField == this.mHourField ? (0 - this.mHourPageUpDownInterval)
: (0 - this.mMinSecPageUpDownInterval));
break;
case "Home":
let min = targetField.getAttribute("min");
this.setFieldValue(targetField, min);
targetField.select();
break;
case "End":
let max = targetField.getAttribute("max");
this.setFieldValue(targetField, max);
targetField.select();
break;
}
this.setInputValueFromFields();
]]>
</body>
</method>
<method name="handleKeypress">
<parameter name="aEvent"/>
<body>
<![CDATA[
if (this.isDisabled() || this.isReadonly()) {
return;
}
let targetField = aEvent.originalTarget;
let key = aEvent.key;
if (this.mDayPeriodField &&
targetField == this.mDayPeriodField) {
if (key == "a" || key == "A") {
this.mDayPeriodField.value = this.mAMIndicator;
this.mDayPeriodField.select();
} else if (key == "p" || key == "P") {
this.mDayPeriodField.value = this.mPMIndicator;
this.mDayPeriodField.select();
}
return;
}
if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
let buffer = targetField.getAttribute("typeBuffer") || "";
buffer = buffer.concat(key);
this.setFieldValue(targetField, buffer);
targetField.select();
let n = Number(buffer);
let max = targetField.getAttribute("max");
if (buffer.length >= targetField.maxLength || n * 10 > max) {
buffer = "";
this.advanceToNextField();
}
targetField.setAttribute("typeBuffer", buffer);
}
]]>
</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 (aField.maxLength == this.mMaxLength) { // For hour, minute and second
if (aField == this.mHourField && this.mHour12) {
value = (value > this.mMaxHourInHour12) ?
value - this.mMaxHourInHour12 : value;
if (aValue == "00") {
value = this.mMaxHourInHour12;
}
}
// prepend zero
if (value < 10) {
value = "0" + value;
}
} else if (aField.maxLength == this.mMillisecMaxLength) {
// prepend zeroes
if (value < 10) {
value = "00" + value;
} else if (value < 100) {
value = "0" + value;
}
}
aField.value = value;
]]>
</body>
</method>
<method name="isValueAvailable">
<body>
<![CDATA[
// Picker only cares about hour:minute.
return !this.isEmpty(this.mHourField.value) ||
!this.isEmpty(this.mMinuteField.value);
]]>
</body>
</method>
<method name="getCurrentValue">
<body>
<![CDATA[
let hour;
if (!this.isEmpty(this.mHourField.value)) {
hour = Number(this.mHourField.value);
if (this.mHour12) {
let dayPeriod = this.mDayPeriodField.value;
if (dayPeriod == this.mPMIndicator &&
hour < this.mMaxHourInHour12) {
hour += this.mMaxHourInHour12;
} else if (dayPeriod == this.mAMIndicator &&
hour == this.mMaxHourInHour12) {
hour = 0;
}
}
}
let minute;
if (!this.isEmpty(this.mMinuteField.value)) {
minute = Number(this.mMinuteField.value);
}
// Picker only needs hour/minute.
let time = { hour, minute };
this.log("getCurrentValue: " + JSON.stringify(time));
return time;
]]>
</body>
</method>
</implementation>
</binding>
<binding id="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>
<content>
<html:div class="datetime-input-box-wrapper"
xbl:inherits="context,disabled,readonly">
<html:span>
<html:input anonid="input-one"
class="textbox-input datetime-input numeric"
size="2" maxlength="2"
xbl:inherits="disabled,readonly,tabindex"/>
<html:span anonid="sep-first" class="datetime-separator"></html:span>
<html:input anonid="input-two"
class="textbox-input datetime-input numeric"
size="2" maxlength="2"
xbl:inherits="disabled,readonly,tabindex"/>
<html:span anonid="sep-second" class="datetime-separator"></html:span>
<html:input anonid="input-three"
class="textbox-input datetime-input numeric"
size="2" maxlength="2"
xbl:inherits="disabled,readonly,tabindex"/>
</html:span>
<html:button class="datetime-reset-button" anoid="reset-button"
tabindex="-1" xbl:inherits="disabled"
onclick="document.getBindingParent(this).clearInputFields(false);"/>
</html:div>
</content>
<implementation implements="nsIDateTimeInputArea">
<constructor>
<![CDATA[
this.DEBUG = false;
this.mInputElement = this.parentNode;
this.mMin = this.mInputElement.min;
this.mMax = this.mInputElement.max;
this.mStep = this.mInputElement.step;
this.mIsPickerOpen = false;
]]>
</constructor>
<method name="log">
<parameter name="aMsg"/>
<body>
<![CDATA[
if (this.DEBUG) {
dump("[DateTimeBox] " + aMsg + "\n");
}
]]>
</body>
</method>
<method name="focusInnerTextBox">
<body>
<![CDATA[
this.log("focusInnerTextBox");
document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus();
]]>
</body>
</method>
<method name="blurInnerTextBox">
<body>
<![CDATA[
this.log("blurInnerTextBox");
if (this.mLastFocusedField) {
this.mLastFocusedField.blur();
}
]]>
</body>
</method>
<method name="notifyInputElementValueChanged">
<body>
<![CDATA[
this.log("inputElementValueChanged");
this.setFieldsFromInputValue();
]]>
</body>
</method>
<method name="setValueFromPicker">
<parameter name="aValue"/>
<body>
<![CDATA[
this.setFieldsFromPicker(aValue);
]]>
</body>
</method>
<method name="advanceToNextField">
<parameter name="aReverse"/>
<body>
<![CDATA[
this.log("advanceToNextField");
let focusedInput = this.mLastFocusedField;
let next = aReverse ? focusedInput.previousElementSibling
: focusedInput.nextElementSibling;
if (!next && !aReverse) {
this.setInputValueFromFields();
return;
}
while (next) {
if (next.type == "text" && !next.disabled) {
next.focus();
break;
}
next = aReverse ? next.previousElementSibling
: next.nextElementSibling;
}
]]>
</body>
</method>
<method name="setPickerState">
<parameter name="aIsOpen"/>
<body>
<![CDATA[
this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
this.mIsPickerOpen = aIsOpen;
]]>
</body>
</method>
<method name="isEmpty">
<parameter name="aValue"/>
<body>
return (aValue == undefined || 0 === aValue.length);
</body>
</method>
<method name="clearInputFields">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="setFieldsFromInputValue">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="setInputValueFromFields">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="setFieldsFromPicker">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="handleKeypress">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="handleKeyboardNav">
<body>
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
</body>
</method>
<method name="notifyPicker">
<body>
<![CDATA[
if (this.mIsPickerOpen && this.isValueAvailable()) {
this.mInputElement.updateDateTimePicker(this.getCurrentValue());
}
]]>
</body>
</method>
<method name="isDisabled">
<body>
<![CDATA[
return this.hasAttribute("disabled");
]]>
</body>
</method>
<method name="isReadonly">
<body>
<![CDATA[
return this.hasAttribute("readonly");
]]>
</body>
</method>
</implementation>
<handlers>
<handler event="focus">
<![CDATA[
this.log("focus on: " + event.originalTarget);
let target = event.originalTarget;
if (target.type == "text") {
this.mLastFocusedField = target;
target.select();
}
]]>
</handler>
<handler event="blur">
<![CDATA[
this.setInputValueFromFields();
]]>
</handler>
<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;
}
if (!(event.originalTarget instanceof HTMLButtonElement)) {
this.mInputElement.openDateTimePicker(this.getCurrentValue());
}
]]>
</handler>
<handler event="keypress" phase="capturing">
<![CDATA[
let key = event.key;
this.log("keypress: " + key);
if (key == "Backspace" || key == "Tab") {
return;
}
if (key == "Enter" || key == " ") {
// Close picker on Enter and Space.
this.mInputElement.closeDateTimePicker();
}
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);
}
event.preventDefault();
]]>
</handler>
</handlers>
</binding>
</bindings>

Просмотреть файл

@ -0,0 +1,165 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const DEBUG = false;
function debug(aStr) {
if (DEBUG) {
dump("-*- DateTimePickerHelper: " + aStr + "\n");
}
}
this.EXPORTED_SYMBOLS = [
"DateTimePickerHelper"
];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
/*
* DateTimePickerHelper receives message from content side (input box) and
* is reposible for opening, closing and updating the picker. Similary,
* DateTimePickerHelper listens for picker's events and notifies the content
* side (input box) about them.
*/
this.DateTimePickerHelper = {
picker: null,
weakBrowser: null,
MESSAGES: [
"FormDateTime:OpenPicker",
"FormDateTime:ClosePicker",
"FormDateTime:UpdatePicker"
],
init: function() {
for (let msg of this.MESSAGES) {
Services.mm.addMessageListener(msg, this);
}
},
uninit: function() {
for (let msg of this.MESSAGES) {
Services.mm.removeMessageListener(msg, this);
}
},
// nsIMessageListener
receiveMessage: function(aMessage) {
debug("receiveMessage: " + aMessage.name);
switch (aMessage.name) {
case "FormDateTime:OpenPicker": {
this.showPicker(aMessage.target, aMessage.data);
break;
}
case "FormDateTime:ClosePicker": {
if (!this.picker) {
return;
}
this.picker.hidePopup();
break;
}
case "FormDateTime:UpdatePicker": {
let value = aMessage.data.value;
debug("Input box value is now: " + value.hour + ":" + value.minute);
// TODO: updating picker will be handled in Bug 1283384.
break;
}
default:
break;
}
},
// nsIDOMEventListener
handleEvent: function(aEvent) {
debug("handleEvent: " + aEvent.type);
switch (aEvent.type) {
case "DateTimePickerValueChanged": {
this.updateInputBoxValue(aEvent);
break;
}
case "popuphidden": {
let browser = this.weakBrowser ? this.weakBrowser.get() : null;
if (browser) {
browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed");
}
this.close();
break;
}
default:
break;
}
},
// 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 });
}
},
// Get picker from browser and show it anchored to the input box.
showPicker: function(aBrowser, aData) {
let rect = aData.rect;
let dir = aData.dir;
let type = aData.type;
let detail = aData.detail;
debug("Opening picker with details: " + JSON.stringify(detail));
let window = aBrowser.ownerDocument.defaultView;
let tabbrowser = window.gBrowser;
if (Services.focus.activeWindow != window ||
tabbrowser.selectedBrowser != aBrowser) {
// We were sent a message from a window or tab that went into the
// background, so we'll ignore it for now.
return;
}
this.weakBrowser = Cu.getWeakReference(aBrowser);
this.picker = aBrowser.dateTimePicker;
if (!this.picker) {
debug("aBrowser.dateTimePicker not found, exiting now.");
return;
}
this.picker.hidden = false;
this.picker.openPopupAtScreenRect("after_start", rect.left, rect.top,
rect.width, rect.height, false, false);
this.addPickerListeners();
},
// Picker is closed, do some cleanup.
close: function() {
this.removePickerListeners();
this.picker = null;
this.weakBrowser = null;
},
// Listen to picker's event.
addPickerListeners: function() {
if (!this.picker) {
return;
}
this.picker.addEventListener("popuphidden", this);
this.picker.addEventListener("DateTimePickerValueChanged", this);
},
// Stop listening to picker's event.
removePickerListeners: function() {
if (!this.picker) {
return;
}
this.picker.removeEventListener("popuphidden", this);
this.picker.removeEventListener("DateTimePickerValueChanged", this);
},
};

Просмотреть файл

@ -35,6 +35,7 @@ EXTRA_JS_MODULES += [
'ClientID.jsm',
'Color.jsm',
'Console.jsm',
'DateTimePickerHelper.jsm',
'debug.js',
'DeferredTask.jsm',
'Deprecated.jsm',

Просмотреть файл

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<path id="Combined-Shape" d="M6,12c3.3,0,6-2.7,6-6S9.3,0,6,0S0,2.7,0,6S2.7,12,6,12z M9,8.1L8.1,9L6,6.9L3.9,9L3,8.1L5.1,6L3,3.9
L3.9,3L6,5.1L8.1,3L9,3.9L6.9,6L9,8.1z"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 521 B

Просмотреть файл

@ -23,6 +23,7 @@ toolkit.jar:
skin/classic/global/config.css (../../shared/config.css)
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/warning.svg (../../shared/incontent-icons/warning.svg)