diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-04-15 11:50:14 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-04-15 11:51:11 +0200 |
commit | fca67bb95d4cda5046365e124e79ec583c336697 (patch) | |
tree | c511e5060d170f181c10155c0a5a3583fe66b756 | |
parent | f8ad954f7d670ba7d4de0648f4b60d3507164dd2 (diff) | |
parent | f4d1b4804a0dad0a616b01edc31187dace152165 (diff) | |
download | UXP-fca67bb95d4cda5046365e124e79ec583c336697.tar UXP-fca67bb95d4cda5046365e124e79ec583c336697.tar.gz UXP-fca67bb95d4cda5046365e124e79ec583c336697.tar.lz UXP-fca67bb95d4cda5046365e124e79ec583c336697.tar.xz UXP-fca67bb95d4cda5046365e124e79ec583c336697.zip |
Merge branch 'html_input_datetime_1'
This is almost everything needed for #162.
83 files changed, 5095 insertions, 699 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..f03f21c3f 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -557,7 +557,7 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks { transition: none; } -#DateTimePickerPanel { +#DateTimePickerPanel[active="true"] { -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup"); } @@ -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/browser/base/content/browser.xul b/browser/base/content/browser.xul index ae531e167..5879f2a29 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -155,13 +155,17 @@ level="parent" overflowpadding="30" /> + <!-- for date/time picker. consumeoutsideclicks is set to never, so that + clicks on the anchored input box are never consumed. --> <panel id="DateTimePickerPanel" type="arrow" hidden="true" orient="vertical" noautofocus="true" - consumeoutsideclicks="false" - level="parent"> + norolluponanchor="true" + consumeoutsideclicks="never" + level="parent" + tabspecific="true"> <iframe id="dateTimePopupFrame"/> </panel> diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index d46eccdbc..f1a54705e 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -543,9 +543,8 @@ GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData, bool HTMLInputElement::ValueAsDateEnabled(JSContext* cx, JSObject* obj) { - return Preferences::GetBool("dom.experimental_forms", false) || - Preferences::GetBool("dom.forms.datepicker", false) || - Preferences::GetBool("dom.forms.datetime", false); + return IsExperimentalFormsEnabled() || IsDatePickerEnabled() || + IsInputDateTimeEnabled(); } NS_IMETHODIMP @@ -628,7 +627,7 @@ HTMLInputElement::nsFilePickerShownCallback::Done(int16_t aResult) RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback = new DispatchChangeEventCallback(mInput); - if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + if (IsWebkitDirPickerEnabled() && mInput->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) { ErrorResult error; GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error); @@ -827,7 +826,7 @@ HTMLInputElement::IsPopupBlocked() const nsresult HTMLInputElement::InitDatePicker() { - if (!Preferences::GetBool("dom.forms.datepicker", false)) { + if (!IsDatePickerEnabled()) { return NS_OK; } @@ -1919,6 +1918,22 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue, aResultValue = Decimal::fromDouble(days * kMsPerDay); return true; } + case NS_FORM_INPUT_DATETIME_LOCAL: + { + uint32_t year, month, day, timeInMs; + if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) { + return false; + } + + JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day, + timeInMs)); + if (!time.isValid()) { + return false; + } + + aResultValue = Decimal::fromDouble(time.toDouble()); + return true; + } default: MOZ_ASSERT(false, "Unrecognized input type"); return false; @@ -2108,21 +2123,17 @@ HTMLInputElement::ConvertNumberToString(Decimal aValue, } case NS_FORM_INPUT_TIME: { + aValue = aValue.floor(); // Per spec, we need to truncate |aValue| and we should only represent // times inside a day [00:00, 24:00[, which means that we should do a // modulo on |aValue| using the number of milliseconds in a day (86400000). - uint32_t value = NS_floorModulo(aValue.floor(), Decimal(86400000)).toDouble(); - - uint16_t milliseconds = value % 1000; - value /= 1000; - - uint8_t seconds = value % 60; - value /= 60; + uint32_t value = + NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); - uint8_t minutes = value % 60; - value /= 60; - - uint8_t hours = value; + uint16_t milliseconds, seconds, minutes, hours; + if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) { + return false; + } if (milliseconds != 0) { aResultString.AppendPrintf("%02d:%02d:%02d.%03d", @@ -2192,6 +2203,42 @@ HTMLInputElement::ConvertNumberToString(Decimal aValue, aResultString.AppendPrintf("%04.0f-W%02d", year, week); return true; } + case NS_FORM_INPUT_DATETIME_LOCAL: + { + aValue = aValue.floor(); + + uint32_t timeValue = + NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); + + uint16_t milliseconds, seconds, minutes, hours; + if (!GetTimeFromMs(timeValue, + &hours, &minutes, &seconds, &milliseconds)) { + return false; + } + + double year = JS::YearFromTime(aValue.toDouble()); + double month = JS::MonthFromTime(aValue.toDouble()); + double day = JS::DayFromTime(aValue.toDouble()); + + if (IsNaN(year) || IsNaN(month) || IsNaN(day)) { + return false; + } + + if (milliseconds != 0) { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d", + year, month + 1, day, hours, minutes, + seconds, milliseconds); + } else if (seconds != 0) { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d", + year, month + 1, day, hours, minutes, + seconds); + } else { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d", + year, month + 1, day, hours, minutes); + } + + return true; + } default: MOZ_ASSERT(false, "Unrecognized input type"); return false; @@ -2202,8 +2249,7 @@ HTMLInputElement::ConvertNumberToString(Decimal aValue, Nullable<Date> HTMLInputElement::GetValueAsDate(ErrorResult& aRv) { - // TODO: this is temporary until bug 888331 is fixed. - if (!IsDateTimeInputType(mType) || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!IsDateTimeInputType(mType)) { return Nullable<Date>(); } @@ -2261,6 +2307,19 @@ HTMLInputElement::GetValueAsDate(ErrorResult& aRv) return Nullable<Date>(Date(time)); } + case NS_FORM_INPUT_DATETIME_LOCAL: + { + uint32_t year, month, day, timeInMs; + nsAutoString value; + GetValueInternal(value); + if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) { + return Nullable<Date>(); + } + + JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day, + timeInMs)); + return Nullable<Date>(Date(time)); + } } MOZ_ASSERT(false, "Unrecognized input type"); @@ -2271,8 +2330,7 @@ HTMLInputElement::GetValueAsDate(ErrorResult& aRv) void HTMLInputElement::SetValueAsDate(Nullable<Date> aDate, ErrorResult& aRv) { - // TODO: this is temporary until bug 888331 is fixed. - if (!IsDateTimeInputType(mType) || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!IsDateTimeInputType(mType)) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } @@ -2380,11 +2438,8 @@ HTMLInputElement::GetMaximum() const Decimal HTMLInputElement::GetStepBase() const { - MOZ_ASSERT(mType == NS_FORM_INPUT_NUMBER || - mType == NS_FORM_INPUT_DATE || - mType == NS_FORM_INPUT_TIME || - mType == NS_FORM_INPUT_MONTH || - mType == NS_FORM_INPUT_WEEK || + MOZ_ASSERT(IsDateTimeInputType(mType) || + mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_RANGE, "Check that kDefaultStepBase is correct for this new type"); @@ -2516,10 +2571,8 @@ bool HTMLInputElement::IsExperimentalMobileType(uint8_t aType) { return (aType == NS_FORM_INPUT_DATE && - !Preferences::GetBool("dom.forms.datetime", false) && - !Preferences::GetBool("dom.forms.datepicker", false)) || - (aType == NS_FORM_INPUT_TIME && - !Preferences::GetBool("dom.forms.datetime", false)); + !IsInputDateTimeEnabled() && !IsDatePickerEnabled()) || + (aType == NS_FORM_INPUT_TIME && !IsInputDateTimeEnabled()); } bool @@ -2832,7 +2885,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; } } @@ -3024,8 +3078,8 @@ HTMLInputElement::GetDisplayFileName(nsAString& aValue) const nsXPIDLString value; if (mFilesOrDirectories.IsEmpty()) { - if ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) || - (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + if ((IsDirPickerEnabled() && Allowdirs()) || + (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) { nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, "NoDirSelected", value); @@ -3054,7 +3108,7 @@ HTMLInputElement::SetFilesOrDirectories(const nsTArray<OwningFileOrDirectory>& a { ClearGetFilesHelpers(); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this); mEntries.Clear(); } @@ -3073,7 +3127,7 @@ HTMLInputElement::SetFiles(nsIDOMFileList* aFiles, mFilesOrDirectories.Clear(); ClearGetFilesHelpers(); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { HTMLInputElementBinding::ClearCachedWebkitEntriesValue(this); mEntries.Clear(); } @@ -3096,14 +3150,14 @@ HTMLInputElement::MozSetDndFilesAndDirectories(const nsTArray<OwningFileOrDirect { SetFilesOrDirectories(aFilesOrDirectories, true); - if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) { + if (IsWebkitFileSystemEnabled()) { UpdateEntries(aFilesOrDirectories); } RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback = new DispatchChangeEventCallback(this); - if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + if (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) { ErrorResult rv; GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */, @@ -3181,8 +3235,8 @@ HTMLInputElement::GetFiles() return nullptr; } - if (Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs() && - (!Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) || + if (IsDirPickerEnabled() && Allowdirs() && + (!IsWebkitDirPickerEnabled() || !HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) { return nullptr; } @@ -3282,7 +3336,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 +3646,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 +3674,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 +4013,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 +4140,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 || @@ -4361,8 +4419,8 @@ HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) do_QueryInterface(aVisitor.mEvent->mOriginalTarget); if (target && target->FindFirstNonChromeOnlyAccessContent() == this && - ((Preferences::GetBool("dom.input.dirpicker", false) && Allowdirs()) || - (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + ((IsDirPickerEnabled() && Allowdirs()) || + (IsWebkitDirPickerEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)))) { type = FILE_PICKER_DIRECTORY; } @@ -5383,6 +5441,29 @@ HTMLInputElement::MaximumWeekInYear(uint32_t aYear) const } bool +HTMLInputElement::GetTimeFromMs(double aValue, uint16_t* aHours, + uint16_t* aMinutes, uint16_t* aSeconds, + uint16_t* aMilliseconds) const { + MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay, + "aValue must be milliseconds within a day!"); + + uint32_t value = floor(aValue); + + *aMilliseconds = value % 1000; + value /= 1000; + + *aSeconds = value % 60; + value /= 60; + + *aMinutes = value % 60; + value /= 60; + + *aHours = value; + + return true; +} + +bool HTMLInputElement::IsValidWeek(const nsAString& aValue) const { uint32_t year, week; @@ -5730,20 +5811,133 @@ HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult) return true; } -static bool -IsDateTimeEnabled(int32_t aNewType) +/* static */ bool +HTMLInputElement::IsDateTimeTypeSupported(uint8_t aDateTimeInputType) +{ + return (aDateTimeInputType == NS_FORM_INPUT_DATE && + (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled() || + IsDatePickerEnabled())) || + (aDateTimeInputType == NS_FORM_INPUT_TIME && + (IsInputDateTimeEnabled() || IsExperimentalFormsEnabled())) || + ((aDateTimeInputType == NS_FORM_INPUT_MONTH || + aDateTimeInputType == NS_FORM_INPUT_WEEK || + aDateTimeInputType == NS_FORM_INPUT_DATETIME_LOCAL) && + IsInputDateTimeEnabled()); +} + +/* static */ bool +HTMLInputElement::IsWebkitDirPickerEnabled() +{ + static bool sWebkitDirPickerEnabled = false; + static bool sWebkitDirPickerPrefCached = false; + if (!sWebkitDirPickerPrefCached) { + sWebkitDirPickerPrefCached = true; + Preferences::AddBoolVarCache(&sWebkitDirPickerEnabled, + "dom.webkitBlink.dirPicker.enabled", + false); + } + + return sWebkitDirPickerEnabled; +} + +/* static */ bool +HTMLInputElement::IsWebkitFileSystemEnabled() +{ + static bool sWebkitFileSystemEnabled = false; + static bool sWebkitFileSystemPrefCached = false; + if (!sWebkitFileSystemPrefCached) { + sWebkitFileSystemPrefCached = true; + Preferences::AddBoolVarCache(&sWebkitFileSystemEnabled, + "dom.webkitBlink.filesystem.enabled", + false); + } + + return sWebkitFileSystemEnabled; +} + +/* static */ bool +HTMLInputElement::IsDirPickerEnabled() +{ + static bool sDirPickerEnabled = false; + static bool sDirPickerPrefCached = false; + if (!sDirPickerPrefCached) { + sDirPickerPrefCached = true; + Preferences::AddBoolVarCache(&sDirPickerEnabled, "dom.input.dirpicker", + false); + } + + return sDirPickerEnabled; +} + +/* static */ bool +HTMLInputElement::IsDatePickerEnabled() { - return (aNewType == NS_FORM_INPUT_DATE && - (Preferences::GetBool("dom.forms.datetime", false) || - Preferences::GetBool("dom.experimental_forms", false) || - Preferences::GetBool("dom.forms.datepicker", false))) || - (aNewType == NS_FORM_INPUT_TIME && - (Preferences::GetBool("dom.forms.datetime", false) || - Preferences::GetBool("dom.experimental_forms", false))) || - ((aNewType == NS_FORM_INPUT_MONTH || - aNewType == NS_FORM_INPUT_WEEK || - aNewType == NS_FORM_INPUT_DATETIME_LOCAL) && - Preferences::GetBool("dom.forms.datetime", false)); + static bool sDatePickerEnabled = false; + static bool sDatePickerPrefCached = false; + if (!sDatePickerPrefCached) { + sDatePickerPrefCached = true; + Preferences::AddBoolVarCache(&sDatePickerEnabled, "dom.forms.datepicker", + false); + } + + return sDatePickerEnabled; +} + +/* static */ bool +HTMLInputElement::IsExperimentalFormsEnabled() +{ + static bool sExperimentalFormsEnabled = false; + static bool sExperimentalFormsPrefCached = false; + if (!sExperimentalFormsPrefCached) { + sExperimentalFormsPrefCached = true; + Preferences::AddBoolVarCache(&sExperimentalFormsEnabled, + "dom.experimental_forms", + false); + } + + return sExperimentalFormsEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputDateTimeEnabled() +{ + static bool sDateTimeEnabled = false; + static bool sDateTimePrefCached = false; + if (!sDateTimePrefCached) { + sDateTimePrefCached = true; + Preferences::AddBoolVarCache(&sDateTimeEnabled, "dom.forms.datetime", + false); + } + + return sDateTimeEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputNumberEnabled() +{ + static bool sInputNumberEnabled = false; + static bool sInputNumberPrefCached = false; + if (!sInputNumberPrefCached) { + sInputNumberPrefCached = true; + Preferences::AddBoolVarCache(&sInputNumberEnabled, "dom.forms.number", + false); + } + + return sInputNumberEnabled; +} + +/* static */ bool +HTMLInputElement::IsInputColorEnabled() +{ + static bool sInputColorEnabled = false; + static bool sInputColorPrefCached = false; + if (!sInputColorPrefCached) { + sInputColorPrefCached = true; + Preferences::AddBoolVarCache(&sInputColorEnabled, "dom.forms.color", + false); + } + + return sInputColorEnabled; } bool @@ -5761,12 +5955,11 @@ HTMLInputElement::ParseAttribute(int32_t aNamespaceID, if (success) { newType = aResult.GetEnumValue(); if ((IsExperimentalMobileType(newType) && - !Preferences::GetBool("dom.experimental_forms", false)) || - (newType == NS_FORM_INPUT_NUMBER && - !Preferences::GetBool("dom.forms.number", false)) || - (newType == NS_FORM_INPUT_COLOR && - !Preferences::GetBool("dom.forms.color", false)) || - (IsDateTimeInputType(newType) && !IsDateTimeEnabled(newType))) { + !IsExperimentalFormsEnabled()) || + (newType == NS_FORM_INPUT_NUMBER && !IsInputNumberEnabled()) || + (newType == NS_FORM_INPUT_COLOR && !IsInputColorEnabled()) || + (IsDateTimeInputType(newType) && + !IsDateTimeTypeSupported(newType))) { newType = kInputDefaultType->value; aResult.SetTo(newType, &aValue); } @@ -7161,13 +7354,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; @@ -7650,8 +7845,7 @@ HTMLInputElement::HasPatternMismatch() const bool HTMLInputElement::IsRangeOverflow() const { - // TODO: this is temporary until bug 888331 is fixed. - if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!DoesMinMaxApply()) { return false; } @@ -7671,8 +7865,7 @@ HTMLInputElement::IsRangeOverflow() const bool HTMLInputElement::IsRangeUnderflow() const { - // TODO: this is temporary until bug 888331 is fixed. - if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!DoesMinMaxApply()) { return false; } @@ -8622,6 +8815,7 @@ HTMLInputElement::GetStepScaleFactor() const case NS_FORM_INPUT_RANGE: return kStepScaleFactorNumberRange; case NS_FORM_INPUT_TIME: + case NS_FORM_INPUT_DATETIME_LOCAL: return kStepScaleFactorTime; case NS_FORM_INPUT_MONTH: return kStepScaleFactorMonth; @@ -8646,6 +8840,7 @@ HTMLInputElement::GetDefaultStep() const case NS_FORM_INPUT_RANGE: return kDefaultStep; case NS_FORM_INPUT_TIME: + case NS_FORM_INPUT_DATETIME_LOCAL: return kDefaultStepTime; default: MOZ_ASSERT(false, "Unrecognized input type"); @@ -8680,8 +8875,7 @@ HTMLInputElement::UpdateHasRange() mHasRange = false; - // TODO: this is temporary until bug 888331 is fixed. - if (!DoesMinMaxApply() || mType == NS_FORM_INPUT_DATETIME_LOCAL) { + if (!DoesMinMaxApply()) { return; } diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h index 9ca876aee..adab663c3 100644 --- a/dom/html/HTMLInputElement.h +++ b/dom/html/HTMLInputElement.h @@ -796,6 +796,15 @@ public: void UpdateDateTimePicker(const DateTimeValue& aValue); void CloseDateTimePicker(); + /* + * The following are called from datetime input box binding to get the + * corresponding computed values. + */ + double GetStepAsDouble() { return GetStep().toDouble(); } + double GetStepBaseAsDouble() { return GetStepBase().toDouble(); } + double GetMinimumAsDouble() { return GetMinimum().toDouble(); } + double GetMaximumAsDouble() { return GetMaximum().toDouble(); } + HTMLInputElement* GetOwnerNumberControl(); HTMLInputElement* GetOwnerDateTimeControl(); @@ -1061,11 +1070,7 @@ protected: /** * Returns if the step attribute apply for the current type. */ - bool DoesStepApply() const - { - // TODO: this is temporary until bug 888331 is fixed. - return DoesMinMaxApply() && mType != NS_FORM_INPUT_DATETIME_LOCAL; - } + bool DoesStepApply() const { return DoesMinMaxApply(); } /** * Returns if stepDown and stepUp methods apply for the current type. @@ -1075,11 +1080,7 @@ protected: /** * Returns if valueAsNumber attribute applies for the current type. */ - bool DoesValueAsNumberApply() const - { - // TODO: this is temporary until bug 888331 is fixed. - return DoesMinMaxApply() && mType != NS_FORM_INPUT_DATETIME_LOCAL; - } + bool DoesValueAsNumberApply() const { return DoesMinMaxApply(); } /** * Returns if autocomplete attribute applies for the current type. @@ -1287,6 +1288,7 @@ protected: * https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string */ void NormalizeDateTimeLocal(nsAString& aValue) const; + /** * This methods returns the number of days since epoch for a given year and * week. @@ -1318,6 +1320,13 @@ protected: uint32_t MaximumWeekInYear(uint32_t aYear) const; /** + * This method converts aValue (milliseconds within a day) to hours, minutes, + * seconds and milliseconds. + */ + bool GetTimeFromMs(double aValue, uint16_t* aHours, uint16_t* aMinutes, + uint16_t* aSeconds, uint16_t* aMilliseconds) const; + + /** * This methods returns true if it's a leap year. */ bool IsLeapYear(uint32_t aYear) const; @@ -1632,9 +1641,73 @@ private: return IsSingleLineTextControl(false, aType) || aType == NS_FORM_INPUT_RANGE || aType == NS_FORM_INPUT_NUMBER || - aType == NS_FORM_INPUT_TIME; + aType == NS_FORM_INPUT_TIME || + aType == NS_FORM_INPUT_DATE; } + /** + * Checks if aDateTimeInputType should be supported based on "dom.forms.datetime", + * "dom.forms.datepicker" and "dom.experimental_forms". + */ + static bool + IsDateTimeTypeSupported(uint8_t aDateTimeInputType); + + /** + * Checks preference "dom.webkitBlink.dirPicker.enabled" to determine if + * webkitdirectory should be supported. + */ + static bool + IsWebkitDirPickerEnabled(); + + /** + * Checks preference "dom.webkitBlink.filesystem.enabled" to determine if + * webkitEntries should be supported. + */ + static bool + IsWebkitFileSystemEnabled(); + + /** + * Checks preference "dom.input.dirpicker" to determine if file and directory + * entries API should be supported. + */ + static bool + IsDirPickerEnabled(); + + /** + * Checks preference "dom.forms.datepicker" to determine if date picker should + * be supported. + */ + static bool + IsDatePickerEnabled(); + + /** + * Checks preference "dom.experimental_forms" to determine if experimental + * implementation of input element should be enabled. + */ + static bool + IsExperimentalFormsEnabled(); + + /** + * Checks preference "dom.forms.datetime" to determine if input date/time + * related types should be supported. + */ + static bool + IsInputDateTimeEnabled(); + + /** + * Checks preference "dom.forms.number" to determine if input type=number + * should be supported. + */ + static bool + IsInputNumberEnabled(); + + /** + * Checks preference "dom.forms.color" to determine if date/time related + * types should be supported. + */ + static bool + IsInputColorEnabled(); + struct nsFilePickerFilter { nsFilePickerFilter() : mFilterMask(0) {} diff --git a/dom/html/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..199e4baf8 100644 --- a/dom/html/test/forms/mochitest.ini +++ b/dom/html/test/forms/mochitest.ini @@ -30,8 +30,14 @@ 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_input_change_events.html] +skip-if = os == "android" [test_input_datetime_focus_blur.html] skip-if = os == "android" +[test_input_datetime_focus_blur_events.html] +skip-if = os == "android" [test_input_datetime_tabindex.html] skip-if = os == "android" [test_input_defaultValue.html] @@ -61,8 +67,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..f502d6a4d --- /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 should have no effect on year field. + keys: ["VK_TAB", "VK_TAB", "VK_HOME"], + initialVal: "2016-01-01", + expectedVal: "2016-01-01" + }, + { + // End key should have no effect on year field. + keys: ["VK_TAB", "VK_TAB", "VK_END"], + initialVal: "2016-01-01", + expectedVal: "2016-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_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html new file mode 100644 index 000000000..e636995d3 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_input_change_events.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1370858 +--> +<head> +<title>Test for Bug 1370858</title> +<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370858">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onchange="++changeEvents[0]" + oninput="++inputEvents[0]"> +<input type="date" id="input_date" onchange="++changeEvents[1]" + oninput="++inputEvents[1]"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** + * Test for Bug 1370858. + * Test that change and input events are (not) fired for date/time inputs. + **/ + +var inputTypes = ["time", "date"]; +var changeEvents = [0, 0]; +var inputEvents = [0, 0]; +var values = ["10:30", "2017-06-08"]; +var expectedValues = [["09:30", "01:30"], ["2017-05-08", "2017-01-08"]]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + for (var i = 0; i < inputTypes.length; i++) { + var input = document.getElementById("input_" + inputTypes[i]); + var inputRect = input.getBoundingClientRect(); + + // Points over the input's reset button + var resetButton_X = inputRect.width - 15; + var resetButton_Y = inputRect.height / 2; + + is(changeEvents[i], 0, "Number of change events should be 0 at start."); + is(inputEvents[i], 0, "Number of input events should be 0 at start."); + + // Test that change and input events are not dispatched setting .value by + // script. + input.value = values[i]; + is(input.value, values[i], "Check that value was set correctly (0)."); + is(changeEvents[i], 0, "Change event should not have dispatched (0)."); + is(inputEvents[i], 0, "Input event should not have dispatched (0)."); + + // Test that change and input events are fired when changing the value using + // up/down keys. + input.focus(); + synthesizeKey("VK_DOWN", {}); + is(input.value, expectedValues[i][0], "Check that value was set correctly (1)."); + is(changeEvents[i], 1, "Change event should be dispatched (1)."); + is(inputEvents[i], 1, "Input event should ne dispatched (1)."); + + // Test that change and input events are fired when changing the value with + // the keyboard. + synthesizeKey("0", {}); + synthesizeKey("1", {}); + is(input.value, expectedValues[i][1], "Check that value was set correctly (2)."); + is(changeEvents[i], 2, "Change event should be dispatched (2)."); + is(inputEvents[i], 2, "Input event should be dispatched (2)."); + + // Test that change and input events are fired when clearing the value using + // the reset button. + synthesizeMouse(input, resetButton_X, resetButton_Y, {}); + is(input.value, "", "Check that value was set correctly (3)."); + is(changeEvents[i], 3, "Change event should be dispatched (3)."); + is(inputEvents[i], 3, "Input event should be dispatched (3)."); + } +} + +</script> +</pre> +</body> +</html> diff --git a/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/html/test/forms/test_max_attribute.html b/dom/html/test/forms/test_max_attribute.html index 4007cfad6..091ad321b 100644 --- a/dom/html/test/forms/test_max_attribute.html +++ b/dom/html/test/forms/test_max_attribute.html @@ -31,8 +31,7 @@ var data = [ { type: 'month', apply: true }, { type: 'week', apply: true }, { type: 'time', apply: true }, - // TODO: temporary set to false until bug 888331 is fixed. - { type: 'datetime-local', apply: false }, + { type: 'datetime-local', apply: true }, { type: 'number', apply: true }, { type: 'range', apply: true }, { type: 'color', apply: false }, @@ -71,7 +70,8 @@ function checkValidity(aElement, aValidity, aApply, aRangeApply) "element overflow status should be " + !aValidity); var overflowMsg = (aElement.type == "date" || aElement.type == "time" || - aElement.type == "month" || aElement.type == "week") ? + aElement.type == "month" || aElement.type == "week" || + aElement.type == "datetime-local") ? ("Please select a value that is no later than " + aElement.max + ".") : ("Please select a value that is no more than " + aElement.max + "."); is(aElement.validationMessage, @@ -148,7 +148,7 @@ for (var test of data) { input.max = '2016-W39'; break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.max = '2016-12-31T23:59:59'; break; default: ok(false, 'please, add a case for this new type (' + input.type + ')'); @@ -421,7 +421,44 @@ for (var test of data) { break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.value = '2016-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59:59'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59:59.123'; + checkValidity(input, false, apply, apply); + + input.value = '2017-01-01T10:00'; + checkValidity(input, false, apply, apply); + + input.max = '2017-01-01T10:00'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T10:00:30'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '2100-01-01T12:00'; + checkValidity(input, false, apply, apply); + + input.max = '0050-12-31T23:59:59.999'; + checkValidity(input, false, apply, apply); + + input.value = '0050-12-31T23:59:59'; + checkValidity(input, true, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); break; } diff --git a/dom/html/test/forms/test_min_attribute.html b/dom/html/test/forms/test_min_attribute.html index 1258babec..22f21de39 100644 --- a/dom/html/test/forms/test_min_attribute.html +++ b/dom/html/test/forms/test_min_attribute.html @@ -31,8 +31,7 @@ var data = [ { type: 'month', apply: true }, { type: 'week', apply: true }, { type: 'time', apply: true }, - // TODO: temporary set to false until bug 888331 is fixed. - { type: 'datetime-local', apply: false }, + { type: 'datetime-local', apply: true }, { type: 'number', apply: true }, { type: 'range', apply: true }, { type: 'color', apply: false }, @@ -71,7 +70,8 @@ function checkValidity(aElement, aValidity, aApply, aRangeApply) "element underflow status should be " + !aValidity); var underflowMsg = (aElement.type == "date" || aElement.type == "time" || - aElement.type == "month" || aElement.type == "week") ? + aElement.type == "month" || aElement.type == "week" || + aElement.type == "datetime-local") ? ("Please select a value that is no earlier than " + aElement.min + ".") : ("Please select a value that is no less than " + aElement.min + "."); is(aElement.validationMessage, @@ -146,10 +146,10 @@ for (var test of data) { input.min = '2016-06'; break; case 'week': - input.min = "2016-W39"; + input.min = '2016-W39'; break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.min = '2017-01-01T00:00'; break; default: ok(false, 'please, add a case for this new type (' + input.type + ')'); @@ -420,7 +420,44 @@ for (var test of data) { checkValidity(input, true, apply, false); break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + input.value = '2017-12-31T23:59'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T00:00:00.123'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59'; + checkValidity(input, false, apply, apply); + + input.min = '2016-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.value = '2015-12-31T23:59'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01T00:00'; + checkValidity(input, false, apply, apply); + + input.value = '10000-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.min = '0010-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '0010-01-01T10:00'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); break; default: ok(false, 'write tests for ' + input.type); diff --git a/dom/html/test/forms/test_step_attribute.html b/dom/html/test/forms/test_step_attribute.html index 31277860c..a14afa461 100644 --- a/dom/html/test/forms/test_step_attribute.html +++ b/dom/html/test/forms/test_step_attribute.html @@ -31,8 +31,7 @@ var data = [ { type: 'month', apply: true }, { type: 'week', apply: true }, { type: 'time', apply: true }, - // TODO: temporary set to false until bug 888331 is fixed. - { type: 'datetime-local', apply: false }, + { type: 'datetime-local', apply: true }, { type: 'number', apply: true }, { type: 'range', apply: true }, { type: 'color', apply: false }, @@ -950,7 +949,104 @@ for (var test of data) { break; case 'datetime-local': - // TODO: this is temporary until bug 888331 is fixed. + // When step is invalid, every datetime is valid + input.step = 0; + input.value = '2017-02-06T12:00'; + checkValidity(input, true, apply); + + input.step = 'foo'; + input.value = '1970-01-01T00:00'; + checkValidity(input, true, apply); + + input.step = '-1'; + input.value = '1969-12-12 00:10'; + checkValidity(input, true, apply); + + input.removeAttribute('step'); + input.value = '1500-01-01T12:00'; + checkValidity(input, true, apply); + + input.step = 'any'; + input.value = '1966-12-12T12:00'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '2017-01-01 12:00'; + checkValidity(input, true, apply); + + // When min is set to a valid datetime, there is a step base. + input.min = '2017-01-01T00:00:00'; + input.step = '2'; + input.value = '2017-01-01T00:00:02'; + checkValidity(input, true, apply); + + input.value = '2017-01-01T00:00:03'; + checkValidity(input, false, apply, + { low: "2017-01-01T00:00:02", high: "2017-01-01T00:00:04" }); + + input.min = '2017-01-01T00:00:05'; + input.value = '2017-01-01T00:00:08'; + checkValidity(input, false, apply, + { low: "2017-01-01T00:00:07", high: "2017-01-01T00:00:09" }); + + input.min = '2000-01-01T00:00'; + input.step = '120'; + input.value = '2000-01-01T00:02'; + checkValidity(input, true, apply); + + // Without any step attribute the datetime is valid + input.removeAttribute('step'); + checkValidity(input, true, apply); + + input.min = '1950-01-01T00:00'; + input.step = '129600'; // 1.5 day + input.value = '1950-01-02T00:00'; + checkValidity(input, false, apply, + { low: "1950-01-01T00:00", high: "1950-01-02T12:00" }); + + input.step = '259200'; // 3 days + input.value = '1950-01-04T12:00'; + checkValidity(input, false, apply, + { low: "1950-01-04T00:00", high: "1950-01-07T00:00" }); + + input.value = '1950-01-10T00:00'; + checkValidity(input, true, apply); + + input.step = '0.5'; // half a second + input.value = '1950-01-01T00:00:00.123'; + checkValidity(input, false, apply, + { low: "1950-01-01T00:00", high: "1950-01-01T00:00:00.500" }); + + input.value = '2000-01-01T12:30:30.600'; + checkValidity(input, false, apply, + { low: "2000-01-01T12:30:30.500", high: "2000-01-01T12:30:31" }); + + input.value = '1950-01-05T00:00:00.500'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1991-01-01T12:00'; + input.value = '1991-01-01T12:00'; + checkValidity(input, true, apply); + + input.value = '1991-01-01T12:00:03'; + checkValidity(input, false, apply, + { low: "1991-01-01T12:00:02.100", high: "1991-01-01T12:00:04.200" }); + + input.value = '1991-01-01T12:00:06.3'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1969-12-20T10:00:05'; + input.value = '1969-12-20T10:00:05'; + checkValidity(input, true, apply); + + input.value = '1969-12-20T10:00:08'; + checkValidity(input, false, apply, + { low: "1969-12-20T10:00:07.100", high: "1969-12-20T10:00:09.200" }); + + input.value = '1969-12-20T10:00:09.200'; + checkValidity(input, true, apply); break; default: diff --git a/dom/html/test/forms/test_stepup_stepdown.html b/dom/html/test/forms/test_stepup_stepdown.html index d96895180..21cde58aa 100644 --- a/dom/html/test/forms/test_stepup_stepdown.html +++ b/dom/html/test/forms/test_stepup_stepdown.html @@ -52,13 +52,8 @@ function checkAvailability() ["time", true], ["month", true], ["week", true], - ["color", false], - ]; - - var todoList = - [ - ["datetime", true], ["datetime-local", true], + ["color", false], ]; var element = document.createElement("input"); @@ -82,27 +77,6 @@ function checkAvailability() } is(exceptionCaught, !data[1], "stepUp() availability is not correct"); } - - for (data of todoList) { - var exceptionCaught = false; - element.type = data[0]; - try { - element.stepDown(); - } catch (e) { - exceptionCaught = true; - } - todo_is(exceptionCaught, !data[1], - "stepDown() availability is not correct"); - - exceptionCaught = false; - try { - element.stepUp(); - } catch (e) { - exceptionCaught = true; - } - todo_is(exceptionCaught, !data[1], - "stepUp() availability is not correct"); - } } function checkStepDown() @@ -509,6 +483,80 @@ function checkStepDown() [ '2016-W01', 'AnY', null, null, 1, null, true ], [ '2016-W01', 'aNy', null, null, 1, null, true ], ]}, + { type: 'datetime-local', data: [ + // Regular case. + [ '2017-02-07T09:30', null, null, null, null, '2017-02-07T09:29', false ], + // Argument testing. + [ '2017-02-07T09:30', null, null, null, 1, '2017-02-07T09:29', false ], + [ '2017-02-07T09:30', null, null, null, 5, '2017-02-07T09:25', false ], + [ '2017-02-07T09:30', null, null, null, -1, '2017-02-07T09:31', false ], + [ '2017-02-07T09:30', null, null, null, 0, '2017-02-07T09:30', false ], + // hour/minutes/seconds wrapping. + [ '2000-01-01T05:00', null, null, null, null, '2000-01-01T04:59', false ], + [ '2000-01-01T05:00:00', 1, null, null, null, '2000-01-01T04:59:59', false ], + [ '2000-01-01T05:00:00', 0.1, null, null, null, '2000-01-01T04:59:59.900', false ], + [ '2000-01-01T05:00:00', 0.01, null, null, null, '2000-01-01T04:59:59.990', false ], + [ '2000-01-01T05:00:00', 0.001, null, null, null, '2000-01-01T04:59:59.999', false ], + // month/year wrapping. + [ '2012-08-01T12:00', null, null, null, 1440, '2012-07-31T12:00', false ], + [ '1969-01-02T12:00', null, null, null, 5760, '1968-12-29T12:00', false ], + [ '1969-12-31T00:00', null, null, null, -1440, '1970-01-01T00:00', false ], + [ '2012-02-29T00:00', null, null, null, -1440, '2012-03-01T00:00', false ], + // stepDown() on '00:00' gives '23:59'. + [ '2017-02-07T00:00', null, null, null, 1, '2017-02-06T23:59', false ], + [ '2017-02-07T00:00', null, null, null, 3, '2017-02-06T23:57', false ], + // Some random step values.. + [ '2017-02-07T16:07', '0.5', null, null, null, '2017-02-07T16:06:59.500', false ], + [ '2017-02-07T16:07', '2', null, null, null, '2017-02-07T16:06:58', false ], + [ '2017-02-07T16:07', '0.25', null, null, 4, '2017-02-07T16:06:59', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 1, '2017-02-07T16:06:59.100', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 2, '2017-02-07T16:06:58', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 10, '2017-02-07T16:06:49.200', false ], + [ '2017-02-07T16:07', '129600', '2017-02-01T00:00', null, 2, '2017-02-05T12:00', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2017-02-07T10:15', '0', null, null, null, '2017-02-07T10:14', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2017-02-07T10:15', '-1', null, null, null, '2017-02-07T10:14', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2017-02-07T10:15', 'foo', null, null, null, '2017-02-07T10:14', false ], + // Min values testing. + [ '2012-02-02T17:02', '60', 'foo', null, 2, '2012-02-02T17:00', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:09', null, null, '2012-02-02T17:09', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:10', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:10', false ], + [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:08', false ], + [ '2012-02-03T20:05', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ], + [ '2012-02-03T18:00', '129600', '2012-02-01T00:00', null, null, '2012-02-02T12:00', false ], + // Max values testing. + [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:11', false ], + [ '2012-02-02T17:15', '120', null, '2012-02-02T17:13', 3, '2012-02-02T17:09', false ], + [ '2012-02-03T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-02T20:05', false ], + [ '2012-02-03T18:00', '129600', null, '2012-02-03T20:00', null, '2012-02-02T06:00', false ], + // Step mismatch. + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:18', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:16', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:18', false ], + [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:17', false ], + [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:16', false ], + [ '2017-02-07T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-06T17:19', false ], + // Clamping. + [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:11', false ], + [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:12', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:18', false ], + [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 15, '2017-02-07T15:00', false ], + [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 2, '2017-02-07T17:00', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1969-12-31T23:59', false ], + // With step = 'any'. + [ '2017-02-07T15:20', 'any', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'ANY', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'AnY', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'aNy', null, null, 1, null, true ], + ]}, ]; for (var test of testData) { @@ -958,6 +1006,78 @@ function checkStepUp() [ '2016-W01', 'AnY', null, null, 1, null, true ], [ '2016-W01', 'aNy', null, null, 1, null, true ], ]}, + { type: 'datetime-local', data: [ + // Regular case. + [ '2017-02-07T17:09', null, null, null, null, '2017-02-07T17:10', false ], + // Argument testing. + [ '2017-02-07T17:10', null, null, null, 1, '2017-02-07T17:11', false ], + [ '2017-02-07T17:10', null, null, null, 5, '2017-02-07T17:15', false ], + [ '2017-02-07T17:10', null, null, null, -1, '2017-02-07T17:09', false ], + [ '2017-02-07T17:10', null, null, null, 0, '2017-02-07T17:10', false ], + // hour/minutes/seconds wrapping. + [ '2000-01-01T04:59', null, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59', 1, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.900', 0.1, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.990', 0.01, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.999', 0.001, null, null, null, '2000-01-01T05:00', false ], + // month/year wrapping. + [ '2012-07-31T12:00', null, null, null, 1440, '2012-08-01T12:00', false ], + [ '1968-12-29T12:00', null, null, null, 5760, '1969-01-02T12:00', false ], + [ '1970-01-01T00:00', null, null, null, -1440, '1969-12-31T00:00', false ], + [ '2012-03-01T00:00', null, null, null, -1440, '2012-02-29T00:00', false ], + // stepUp() on '23:59' gives '00:00'. + [ '2017-02-07T23:59', null, null, null, 1, '2017-02-08T00:00', false ], + [ '2017-02-07T23:59', null, null, null, 3, '2017-02-08T00:02', false ], + // Some random step values.. + [ '2017-02-07T17:40', '0.5', null, null, null, '2017-02-07T17:40:00.500', false ], + [ '2017-02-07T17:40', '2', null, null, null, '2017-02-07T17:40:02', false ], + [ '2017-02-07T17:40', '0.25', null, null, 4, '2017-02-07T17:40:01', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 1, '2017-02-07T17:40:00.200', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 2, '2017-02-07T17:40:01.300', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 10, '2017-02-07T17:40:10.100', false ], + [ '2017-02-07T17:40', '129600', '2017-02-01T00:00', null, 2, '2017-02-10T00:00', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2017-02-07T17:39', '0', null, null, null, '2017-02-07T17:40', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2017-02-07T17:39', '-1', null, null, null, '2017-02-07T17:40', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2017-02-07T17:39', 'foo', null, null, null, '2017-02-07T17:40', false ], + // Min values testing. + [ '2012-02-02T17:00', '60', 'foo', null, 2, '2012-02-02T17:02', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:11', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:30', false ], + [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:11', false ], + [ '2012-02-02T17:10', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ], + [ '2012-02-02T17:10', '129600', '2012-02-01T00:00', null, null, '2012-02-04T00:00', false ], + // Max values testing. + [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:16', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:16', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:15', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:15', false ], + [ '2012-02-02T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-03T20:05', false ], + [ '2012-02-02T18:00', '129600', null, '2012-02-04T20:00', null, '2012-02-04T06:00', false ], + // Step mismatch. + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:22', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:21', false ], + [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:22', false ], + [ '2017-02-03T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-04T17:19', false ], + // Clamping. + [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:22', false ], + [ '2017-02-06T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:20', 15, '2017-02-06T19:50', false ], + [ '2017-02-06T17:22', '600', '2017-02-02T17:10', '2017-02-07T17:20', 2, '2017-02-06T17:40', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-01-01T00:01', false ], + // With step = 'any'. + [ '2017-02-07T17:30', 'any', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'ANY', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'AnY', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'aNy', null, null, 1, null, true ], + ]}, ]; for (var test of testData) { diff --git a/dom/html/test/forms/test_valueasdate_attribute.html b/dom/html/test/forms/test_valueasdate_attribute.html index 8c19fefd9..65cab3b8e 100644 --- a/dom/html/test/forms/test_valueasdate_attribute.html +++ b/dom/html/test/forms/test_valueasdate_attribute.html @@ -47,8 +47,7 @@ var validTypes = ["color", false], ["month", true], ["week", true], - // TODO: temporary set to false until bug 888331 is fixed. - ["datetime-local", false], + ["datetime-local", true], ]; function checkAvailability() @@ -622,6 +621,107 @@ function checkWeekSet() } } +function checkDatetimeLocalGet() +{ + var validData = + [ + // Simple cases. + [ "2016-12-27T10:30", Date.UTC(2016, 11, 27, 10, 30, 0) ], + [ "2016-12-27T10:30:40", Date.UTC(2016, 11, 27, 10, 30, 40) ], + [ "2016-12-27T10:30:40.567", Date.UTC(2016, 11, 27, 10, 30, 40, 567) ], + [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ], + [ "1970-01-01T00:00", 0 ], + // Leap years. + [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ], + [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ], + [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ], + [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ], + [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ "275760-09-13T00:00", 8640000000000000 ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ "0001-01-01T00:00", -62135596800000 ], + ]; + + var invalidData = + [ + [ "invaliddateime-local" ], + [ "0000-01-01T00:00" ], + [ "2016-12-25T00:00Z" ], + [ "2015-02-29T12:34" ], + [ "1-1-1T12:00" ], + [ "" ], + // This datetime-local is valid for the input element, but is out of the + // date object range. In this case, on getting valueAsDate, a Date object + // will be created, but it will have a NaN internal value, and will return + // the string "Invalid Date". + [ "275760-09-13T12:00", true ], + ]; + + element.type = "datetime-local"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsDate.valueOf(), data[1], + "valueAsDate should return the " + + "valid date object representing this datetime-local"); + } + + for (let data of invalidData) { + element.value = data[0]; + if (data[1]) { + is(String(element.valueAsDate), "Invalid Date", + "valueAsDate should return an invalid Date object " + + "when the element value is not a valid datetime-local"); + } else { + is(element.valueAsDate, null, + "valueAsDate should return null " + + "when the element value is not a valid datetime-local"); + } + } +} + +function checkDatetimeLocalSet() +{ + var testData = + [ + // Simple cases. + [ Date.UTC(2016, 11, 27, 10, 30, 0), "2016-12-27T10:30" ], + [ Date.UTC(2016, 11, 27, 10, 30, 30), "2016-12-27T10:30:30" ], + [ Date.UTC(1999, 11, 31, 23, 59, 59), "1999-12-31T23:59:59" ], + [ Date.UTC(1999, 11, 31, 23, 59, 59, 999), "1999-12-31T23:59:59.999" ], + [ Date.UTC(123456, 7, 8, 9, 10), "123456-08-08T09:10" ], + [ 0, "1970-01-01T00:00" ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13T00:00" ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ -62135596800000, "0001-01-01T00:00" ], + // Leap years. + [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ], + [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ], + [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ], + [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ], + [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ], + // "Values must be truncated to valid datetime-local" + [ 123.123456789123, "1970-01-01T00:00:00.123" ], + [ 1e-1, "1970-01-01T00:00" ], + [ -1.1, "1969-12-31T23:59:59.999" ], + [ -345600000, "1969-12-28T00:00" ], + // Negative years, this is out of range for the input element, + // the corresponding datetime-local string is the empty string + [ -62135596800001, "" ], + ]; + + element.type = "datetime-local"; + for (let data of testData) { + element.valueAsDate = new Date(data[0]); + is(element.value, data[1], "valueAsDate should set the value to " + + data[1]); + element.valueAsDate = new testFrame.Date(data[0]); + is(element.value, data[1], "valueAsDate with other-global date should " + + "set the value to " + data[1]); + } +} + checkAvailability(); checkGarbageValues(); checkWithBustedPrototype(); @@ -642,6 +742,10 @@ checkMonthSet(); checkWeekGet(); checkWeekSet(); +// Test <input type='datetime-local'>. +checkDatetimeLocalGet(); +checkDatetimeLocalSet(); + </script> </pre> </body> diff --git a/dom/html/test/forms/test_valueasnumber_attribute.html b/dom/html/test/forms/test_valueasnumber_attribute.html index d7471502b..2660fc7ed 100644 --- a/dom/html/test/forms/test_valueasnumber_attribute.html +++ b/dom/html/test/forms/test_valueasnumber_attribute.html @@ -46,8 +46,7 @@ function checkAvailability() ["color", false], ["month", true], ["week", true], - // TODO: temporary set to false until bug 888331 is fixed. - ["datetime-local", false], + ["datetime-local", true], ]; var element = document.createElement('input'); @@ -612,7 +611,6 @@ function checkWeekGet() var element = document.createElement('input'); element.type = "week"; for (let data of validData) { - dump("Test: " + data[0]); element.value = data[0]; is(element.valueAsNumber, data[1], "valueAsNumber should return the " + "integer value representing this week"); @@ -698,7 +696,120 @@ function checkWeekSet() try { element.valueAsNumber = data[0]; - is(element.value, data[1], "valueAsNumber should set the value to " + data[1]); + is(element.value, data[1], "valueAsNumber should set the value to " + + data[1]); + } catch(e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "the value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +function checkDatetimeLocalGet() { + var validData = + [ + // Simple cases. + [ "2016-12-20T09:58", Date.UTC(2016, 11, 20, 9, 58) ], + [ "2016-12-20T09:58:30", Date.UTC(2016, 11, 20, 9, 58, 30) ], + [ "2016-12-20T09:58:30.123", Date.UTC(2016, 11, 20, 9, 58, 30, 123) ], + [ "2017-01-01T10:00", Date.UTC(2017, 0, 1, 10, 0, 0) ], + [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ], + [ "1970-01-01T00:00", 0 ], + // Leap years. + [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ], + [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ], + [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ], + [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ], + [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ "275760-09-13T00:00", 8640000000000000 ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ "0001-01-01T00:00", -62135596800000 ], + ]; + + var invalidData = + [ + "invaliddatetime-local", + "0000-01-01T00:00", + "2016-12-25T00:00Z", + "2015-02-29T12:34", + "1-1-1T12:00", + // Out of range. + "275760-09-13T12:00", + ]; + + var element = document.createElement('input'); + element.type = "datetime-local"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "integer value representing this datetime-local"); + } + + for (let data of invalidData) { + element.value = data; + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a valid datetime-local"); + } +} + +function checkDatetimeLocalSet() +{ + var testData = + [ + // Simple cases. + [ Date.UTC(2016, 11, 20, 9, 58, 0), "2016-12-20T09:58", ], + [ Date.UTC(2016, 11, 20, 9, 58, 30), "2016-12-20T09:58:30" ], + [ Date.UTC(2016, 11, 20, 9, 58, 30, 123), "2016-12-20T09:58:30.123" ], + [ Date.UTC(2017, 0, 1, 10, 0, 0), "2017-01-01T10:00" ], + [ Date.UTC(1969, 11, 31, 12, 0, 0), "1969-12-31T12:00" ], + [ 0, "1970-01-01T00:00" ], + // Maximum valid week (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13T00:00" ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ -62135596800000, "0001-01-01T00:00" ], + // Leap years. + [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ], + [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ], + [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ], + [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ], + [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ], + // "Values must be truncated to valid datetime-local" + [ 0.3, "1970-01-01T00:00" ], + [ 1e-1, "1970-01-01T00:00" ], + [ -1 , "1969-12-31T23:59:59.999" ], + [ -345600000, "1969-12-28T00:00" ], + // Invalid numbers. + // Those are implicitly converted to numbers + [ "", "1970-01-01T00:00" ], + [ true, "1970-01-01T00:00:00.001" ], + [ false, "1970-01-01T00:00" ], + [ null, "1970-01-01T00:00" ], + // Those are converted to NaN, the corresponding week string is the empty string + [ "invaliddatetime-local", "" ], + [ NaN, "" ], + [ undefined, "" ], + // Infinity will keep the current value and throw (so we need to set a current value). + [ Date.UTC(2016, 11, 27, 15, 10, 0), "2016-12-27T15:10" ], + [ Infinity, "2016-12-27T15:10", true ], + [ -Infinity, "2016-12-27T15:10", true ], + ]; + + var element = document.createElement('input'); + element.type = "datetime-local"; + for (let data of testData) { + var caught = false; + + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], "valueAsNumber should set the value to " + + data[1]); } catch(e) { caught = true; } @@ -738,6 +849,10 @@ checkMonthSet(); checkWeekGet(); checkWeekSet(); +// <input type='datetime-local'> test +checkDatetimeLocalGet(); +checkDatetimeLocalSet(); + </script> </pre> </body> diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl index 050d19510..cf3e9a4c7 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 { @@ -250,6 +253,14 @@ partial interface HTMLInputElement { [Pref="dom.forms.datetime", ChromeOnly] void setDateTimePickerState(boolean open); + [Pref="dom.forms.datetime", ChromeOnly, + BinaryName="getMinimumAsDouble"] + double getMinimum(); + + [Pref="dom.forms.datetime", ChromeOnly, + BinaryName="getMaximumAsDouble"] + double getMaximum(); + [Pref="dom.forms.datetime", Func="IsChromeOrXBL"] void openDateTimePicker(optional DateTimeValue initialValue); @@ -258,4 +269,12 @@ partial interface HTMLInputElement { [Pref="dom.forms.datetime", Func="IsChromeOrXBL"] void closeDateTimePicker(); + + [Pref="dom.forms.datetime", Func="IsChromeOrXBL", + BinaryName="getStepAsDouble"] + double getStep(); + + [Pref="dom.forms.datetime", Func="IsChromeOrXBL", + BinaryName="getStepBaseAsDouble"] + double getStepBase(); }; diff --git a/js/public/Date.h b/js/public/Date.h index cba0ea875..cab36a3de 100644 --- a/js/public/Date.h +++ b/js/public/Date.h @@ -134,6 +134,14 @@ NewDateObject(JSContext* cx, ClippedTime time); JS_PUBLIC_API(double) MakeDate(double year, unsigned month, unsigned day); +// Year is a year, month is 0-11, day is 1-based, and time is in milliseconds. +// The return value is a number of milliseconds since the epoch. +// +// Consistent with the MakeDate algorithm defined in ECMAScript, this value is +// *not* clipped! Use JS::TimeClip if you need a clipped date. +JS_PUBLIC_API(double) +MakeDate(double year, unsigned month, unsigned day, double time); + // Takes an integer number of milliseconds since the epoch and returns the // year. Can return NaN, and will do so if NaN is passed in. JS_PUBLIC_API(double) diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 3a20c487b..cd808cb10 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -102,6 +102,18 @@ Char16ToUChar(char16_t* chars) MOZ_CRASH("Char16ToUChar: Intl API disabled"); } +inline char16_t* +UCharToChar16(UChar* chars) +{ + MOZ_CRASH("UCharToChar16: Intl API disabled"); +} + +inline const char16_t* +UCharToChar16(const UChar* chars) +{ + MOZ_CRASH("UCharToChar16: Intl API disabled"); +} + static int32_t u_strlen(const UChar* s) { @@ -356,6 +368,27 @@ enum UCalendarDateFields { UCAL_DAY_OF_MONTH = UCAL_DATE }; +enum UCalendarMonths { + UCAL_JANUARY, + UCAL_FEBRUARY, + UCAL_MARCH, + UCAL_APRIL, + UCAL_MAY, + UCAL_JUNE, + UCAL_JULY, + UCAL_AUGUST, + UCAL_SEPTEMBER, + UCAL_OCTOBER, + UCAL_NOVEMBER, + UCAL_DECEMBER, + UCAL_UNDECIMBER +}; + +enum UCalendarAMPMs { + UCAL_AM, + UCAL_PM +}; + static UCalendar* ucal_open(const UChar* zoneID, int32_t len, const char* locale, UCalendarType type, UErrorCode* status) @@ -420,6 +453,13 @@ ucal_getDefaultTimeZone(UChar* result, int32_t resultCapacity, UErrorCode* statu MOZ_CRASH("ucal_getDefaultTimeZone: Intl API disabled"); } +enum UDateTimePatternField { + UDATPG_YEAR_FIELD, + UDATPG_MONTH_FIELD, + UDATPG_WEEK_OF_YEAR_FIELD, + UDATPG_DAY_FIELD, +}; + typedef void* UDateTimePatternGenerator; static UDateTimePatternGenerator* @@ -436,6 +476,14 @@ udatpg_getBestPattern(UDateTimePatternGenerator* dtpg, const UChar* skeleton, MOZ_CRASH("udatpg_getBestPattern: Intl API disabled"); } +static const UChar * +udatpg_getAppendItemName(const UDateTimePatternGenerator *dtpg, + UDateTimePatternField field, + int32_t *pLength) +{ + MOZ_CRASH("udatpg_getAppendItemName: Intl API disabled"); +} + static void udatpg_close(UDateTimePatternGenerator* dtpg) { @@ -488,10 +536,46 @@ enum UDateFormatField { }; enum UDateFormatStyle { + UDAT_FULL, + UDAT_LONG, + UDAT_MEDIUM, + UDAT_SHORT, + UDAT_DEFAULT = UDAT_MEDIUM, UDAT_PATTERN = -2, UDAT_IGNORE = UDAT_PATTERN }; +enum UDateFormatSymbolType { + UDAT_ERAS, + UDAT_MONTHS, + UDAT_SHORT_MONTHS, + UDAT_WEEKDAYS, + UDAT_SHORT_WEEKDAYS, + UDAT_AM_PMS, + UDAT_LOCALIZED_CHARS, + UDAT_ERA_NAMES, + UDAT_NARROW_MONTHS, + UDAT_NARROW_WEEKDAYS, + UDAT_STANDALONE_MONTHS, + UDAT_STANDALONE_SHORT_MONTHS, + UDAT_STANDALONE_NARROW_MONTHS, + UDAT_STANDALONE_WEEKDAYS, + UDAT_STANDALONE_SHORT_WEEKDAYS, + UDAT_STANDALONE_NARROW_WEEKDAYS, + UDAT_QUARTERS, + UDAT_SHORT_QUARTERS, + UDAT_STANDALONE_QUARTERS, + UDAT_STANDALONE_SHORT_QUARTERS, + UDAT_SHORTER_WEEKDAYS, + UDAT_STANDALONE_SHORTER_WEEKDAYS, + UDAT_CYCLIC_YEARS_WIDE, + UDAT_CYCLIC_YEARS_ABBREVIATED, + UDAT_CYCLIC_YEARS_NARROW, + UDAT_ZODIAC_NAMES_WIDE, + UDAT_ZODIAC_NAMES_ABBREVIATED, + UDAT_ZODIAC_NAMES_NARROW +}; + static int32_t udat_countAvailable() { @@ -563,6 +647,13 @@ udat_close(UDateFormat* format) MOZ_CRASH("udat_close: Intl API disabled"); } +static int32_t +udat_getSymbols(const UDateFormat *fmt, UDateFormatSymbolType type, int32_t symbolIndex, + UChar *result, int32_t resultLength, UErrorCode *status) +{ + MOZ_CRASH("udat_getSymbols: Intl API disabled"); +} + #endif @@ -2921,6 +3012,296 @@ js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) return true; } +template<size_t N> +inline bool +MatchPart(const char** pattern, const char (&part)[N]) +{ + if (strncmp(*pattern, part, N - 1)) + return false; + + *pattern += N - 1; + return true; +} + +bool +js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + // 1. Assert: locale is a string. + MOZ_ASSERT(args[0].isString()); + // 2. Assert: style is a string. + MOZ_ASSERT(args[1].isString()); + // 3. Assert: keys is an Array. + MOZ_ASSERT(args[2].isObject()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSAutoByteString style(cx, args[1].toString()); + if (!style) + return false; + + RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>()); + if (!keys) + return false; + + // 4. Let result be ArrayCreate(0). + RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length())); + if (!result) + return false; + + UErrorCode status = U_ZERO_ERROR; + + UDateFormat* fmt = + udat_open(UDAT_DEFAULT, UDAT_DEFAULT, icuLocale(locale.ptr()), + nullptr, 0, nullptr, 0, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject<UDateFormat, udat_close> datToClose(fmt); + + // UDateTimePatternGenerator will be needed for translations of date and + // time fields like "month", "week", "day" etc. + UDateTimePatternGenerator* dtpg = udatpg_open(icuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject<UDateTimePatternGenerator, udatpg_close> datPgToClose(dtpg); + + RootedValue keyValue(cx); + RootedString keyValStr(cx); + RootedValue wordVal(cx); + Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + // 5. For each element of keys, + for (uint32_t i = 0; i < keys->length(); i++) { + /** + * We iterate over keys array looking for paths that we have code + * branches for. + * + * For any unknown path branch, the wordVal will keep NullValue and + * we'll throw at the end. + */ + + if (!GetElement(cx, keys, keys, i, &keyValue)) + return false; + + JSAutoByteString pattern; + keyValStr = keyValue.toString(); + if (!pattern.encodeUtf8(cx, keyValStr)) + return false; + + wordVal.setNull(); + + // 5.a. Perform an implementation dependent algorithm to map a key to a + // corresponding display name. + const char* pat = pattern.ptr(); + + if (!MatchPart(&pat, "dates")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (MatchPart(&pat, "fields")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + UDateTimePatternField fieldType; + + if (MatchPart(&pat, "year")) { + fieldType = UDATPG_YEAR_FIELD; + } else if (MatchPart(&pat, "month")) { + fieldType = UDATPG_MONTH_FIELD; + } else if (MatchPart(&pat, "week")) { + fieldType = UDATPG_WEEK_OF_YEAR_FIELD; + } else if (MatchPart(&pat, "day")) { + fieldType = UDATPG_DAY_FIELD; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + // This part must be the final part with no trailing data. + if (*pat != '\0') { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + int32_t resultSize; + + const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + JSString* word = NewStringCopyN<CanGC>(cx, UCharToChar16(value), resultSize); + if (!word) + return false; + + wordVal.setString(word); + } else if (MatchPart(&pat, "gregorian")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + UDateFormatSymbolType symbolType; + int32_t index; + + if (MatchPart(&pat, "months")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (equal(style, "narrow")) { + symbolType = UDAT_STANDALONE_NARROW_MONTHS; + } else if (equal(style, "short")) { + symbolType = UDAT_STANDALONE_SHORT_MONTHS; + } else { + MOZ_ASSERT(equal(style, "long")); + symbolType = UDAT_STANDALONE_MONTHS; + } + + if (MatchPart(&pat, "january")) { + index = UCAL_JANUARY; + } else if (MatchPart(&pat, "february")) { + index = UCAL_FEBRUARY; + } else if (MatchPart(&pat, "march")) { + index = UCAL_MARCH; + } else if (MatchPart(&pat, "april")) { + index = UCAL_APRIL; + } else if (MatchPart(&pat, "may")) { + index = UCAL_MAY; + } else if (MatchPart(&pat, "june")) { + index = UCAL_JUNE; + } else if (MatchPart(&pat, "july")) { + index = UCAL_JULY; + } else if (MatchPart(&pat, "august")) { + index = UCAL_AUGUST; + } else if (MatchPart(&pat, "september")) { + index = UCAL_SEPTEMBER; + } else if (MatchPart(&pat, "october")) { + index = UCAL_OCTOBER; + } else if (MatchPart(&pat, "november")) { + index = UCAL_NOVEMBER; + } else if (MatchPart(&pat, "december")) { + index = UCAL_DECEMBER; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else if (MatchPart(&pat, "weekdays")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (equal(style, "narrow")) { + symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; + } else if (equal(style, "short")) { + symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS; + } else { + MOZ_ASSERT(equal(style, "long")); + symbolType = UDAT_STANDALONE_WEEKDAYS; + } + + if (MatchPart(&pat, "monday")) { + index = UCAL_MONDAY; + } else if (MatchPart(&pat, "tuesday")) { + index = UCAL_TUESDAY; + } else if (MatchPart(&pat, "wednesday")) { + index = UCAL_WEDNESDAY; + } else if (MatchPart(&pat, "thursday")) { + index = UCAL_THURSDAY; + } else if (MatchPart(&pat, "friday")) { + index = UCAL_FRIDAY; + } else if (MatchPart(&pat, "saturday")) { + index = UCAL_SATURDAY; + } else if (MatchPart(&pat, "sunday")) { + index = UCAL_SUNDAY; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else if (MatchPart(&pat, "dayperiods")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + symbolType = UDAT_AM_PMS; + + if (MatchPart(&pat, "am")) { + index = UCAL_AM; + } else if (MatchPart(&pat, "pm")) { + index = UCAL_PM; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + // This part must be the final part with no trailing data. + if (*pat != '\0') { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + int32_t resultSize = + udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), + INITIAL_CHAR_BUFFER_SIZE, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + if (!chars.resize(resultSize)) + return false; + status = U_ZERO_ERROR; + udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), + resultSize, &status); + } + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + JSString* word = NewStringCopyN<CanGC>(cx, chars.begin(), resultSize); + if (!word) + return false; + + wordVal.setString(word); + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + MOZ_ASSERT(wordVal.isString()); + + // 5.b. Append the result string to result. + if (!DefineElement(cx, result, i, wordVal)) + return false; + } + + // 6. Return result. + args.rval().setObject(*result); + return true; +} + /******************** Intl ********************/ const Class js::IntlClass = { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 54764605b..b2197060d 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -387,6 +387,48 @@ intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp); +/** + * Returns an Array with CLDR-based fields display names. + * The function takes three arguments: + * + * locale + * BCP47 compliant locale string + * style + * A string with values: long or short or narrow + * keys + * An array or path-like strings that identify keys to be returned + * At the moment the following types of keys are supported: + * + * 'dates/fields/{year|month|week|day}' + * 'dates/gregorian/months/{january|...|december}' + * 'dates/gregorian/weekdays/{sunday|...|saturday}' + * 'dates/gregorian/dayperiods/{am|pm}' + * + * Example: + * + * let info = intl_ComputeDisplayNames( + * 'en-US', + * 'long', + * [ + * 'dates/fields/year', + * 'dates/gregorian/months/january', + * 'dates/gregorian/weekdays/monday', + * 'dates/gregorian/dayperiods/am', + * ] + * ); + * + * Returned value: + * + * [ + * 'year', + * 'January', + * 'Monday', + * 'AM' + * ] + */ +extern MOZ_MUST_USE bool +intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp); + #if ENABLE_INTL_API /** * Cast char16_t* strings to UChar* strings used by ICU. @@ -402,6 +444,18 @@ Char16ToUChar(char16_t* chars) { return reinterpret_cast<UChar*>(chars); } + +inline char16_t* +UCharToChar16(UChar* chars) +{ + return reinterpret_cast<char16_t*>(chars); +} + +inline const char16_t* +UCharToChar16(const UChar* chars) +{ + return reinterpret_cast<const char16_t*>(chars); +} #endif // ENABLE_INTL_API } // namespace js diff --git a/js/src/js.msg b/js/src/js.msg index 8d492f523..a276dab94 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -474,6 +474,8 @@ MSG_DEF(JSMSG_INTL_OBJECT_NOT_INITED, 3, JSEXN_TYPEERR, "Intl.{0}.prototype.{1} MSG_DEF(JSMSG_INTL_OBJECT_REINITED, 0, JSEXN_TYPEERR, "can't initialize object twice as an object of an Intl constructor") MSG_DEF(JSMSG_INVALID_CURRENCY_CODE, 1, JSEXN_RANGEERR, "invalid currency code in NumberFormat(): {0}") MSG_DEF(JSMSG_INVALID_DIGITS_VALUE, 1, JSEXN_RANGEERR, "invalid digits value: {0}") +MSG_DEF(JSMSG_INVALID_KEYS_TYPE, 0, JSEXN_TYPEERR, "calendar info keys must be an object or undefined") +MSG_DEF(JSMSG_INVALID_KEY, 1, JSEXN_RANGEERR, "invalid key: {0}") MSG_DEF(JSMSG_INVALID_LANGUAGE_TAG, 1, JSEXN_RANGEERR, "invalid language tag: {0}") MSG_DEF(JSMSG_INVALID_LOCALES_ELEMENT, 0, JSEXN_TYPEERR, "invalid element in locales argument") MSG_DEF(JSMSG_INVALID_LOCALE_MATCHER, 1, JSEXN_RANGEERR, "invalid locale matcher in supportedLocalesOf(): {0}") diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index ccaeda2a3..00a8abf84 100755 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -354,10 +354,22 @@ MakeDate(double day, double time) JS_PUBLIC_API(double) JS::MakeDate(double year, unsigned month, unsigned day) { + MOZ_ASSERT(month <= 11); + MOZ_ASSERT(day >= 1 && day <= 31); + return ::MakeDate(MakeDay(year, month, day), 0); } JS_PUBLIC_API(double) +JS::MakeDate(double year, unsigned month, unsigned day, double time) +{ + MOZ_ASSERT(month <= 11); + MOZ_ASSERT(day >= 1 && day <= 31); + + return ::MakeDate(MakeDay(year, month, day), time); +} + +JS_PUBLIC_API(double) JS::YearFromTime(double time) { return ::YearFromTime(time); diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index cc68c90d5..8d69ca942 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -906,6 +906,7 @@ AddIntlExtras(JSContext* cx, unsigned argc, Value* vp) static const JSFunctionSpec funcs[] = { JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0), + JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0), JS_FS_END }; diff --git a/js/src/tests/Intl/getDisplayNames.js b/js/src/tests/Intl/getDisplayNames.js new file mode 100644 index 000000000..ad2dbc940 --- /dev/null +++ b/js/src/tests/Intl/getDisplayNames.js @@ -0,0 +1,238 @@ +// |reftest| skip-if(!this.hasOwnProperty('Intl')||!this.hasOwnProperty('addIntlExtras')) +/* 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/. */ + +// Tests the getCalendarInfo function with a diverse set of arguments. + +/* + * Test if getDisplayNames return value matches expected values. + */ +function checkDisplayNames(names, expected) +{ + assertEq(Object.getPrototypeOf(names), Object.prototype); + + assertEq(names.locale, expected.locale); + assertEq(names.style, expected.style); + + const nameValues = names.values; + const expectedValues = expected.values; + + const nameValuesKeys = Object.getOwnPropertyNames(nameValues).sort(); + const expectedValuesKeys = Object.getOwnPropertyNames(expectedValues).sort(); + + assertEqArray(nameValuesKeys, expectedValuesKeys); + + for (let key of expectedValuesKeys) + assertEq(nameValues[key], expectedValues[key]); +} + +addIntlExtras(Intl); + +let gDN = Intl.getDisplayNames; + +assertEq(gDN.length, 2); + +checkDisplayNames(gDN('en-US', { +}), { + locale: 'en-US', + style: 'long', + values: {} +}); + +checkDisplayNames(gDN('en-US', { + keys: [ + 'dates/gregorian/weekdays/wednesday' + ], + style: 'narrow' +}), { + locale: 'en-US', + style: 'narrow', + values: { + 'dates/gregorian/weekdays/wednesday': 'W' + } +}); + +checkDisplayNames(gDN('en-US', { + keys: [ + 'dates/fields/year', + 'dates/fields/month', + 'dates/fields/week', + 'dates/fields/day', + 'dates/gregorian/months/january', + 'dates/gregorian/months/february', + 'dates/gregorian/months/march', + 'dates/gregorian/weekdays/tuesday' + ] +}), { + locale: 'en-US', + style: 'long', + values: { + 'dates/fields/year': 'year', + 'dates/fields/month': 'month', + 'dates/fields/week': 'week', + 'dates/fields/day': 'day', + 'dates/gregorian/months/january': 'January', + 'dates/gregorian/months/february': 'February', + 'dates/gregorian/months/march': 'March', + 'dates/gregorian/weekdays/tuesday': 'Tuesday', + } +}); + +checkDisplayNames(gDN('fr', { + keys: [ + 'dates/fields/year', + 'dates/fields/day', + 'dates/gregorian/months/october', + 'dates/gregorian/weekdays/saturday', + 'dates/gregorian/dayperiods/pm' + ] +}), { + locale: 'fr', + style: 'long', + values: { + 'dates/fields/year': 'année', + 'dates/fields/day': 'jour', + 'dates/gregorian/months/october': 'octobre', + 'dates/gregorian/weekdays/saturday': 'samedi', + 'dates/gregorian/dayperiods/pm': 'PM' + } +}); + +checkDisplayNames(gDN('it', { + style: 'short', + keys: [ + 'dates/gregorian/weekdays/thursday', + 'dates/gregorian/months/august', + 'dates/gregorian/dayperiods/am', + 'dates/fields/month', + ] +}), { + locale: 'it', + style: 'short', + values: { + 'dates/gregorian/weekdays/thursday': 'gio', + 'dates/gregorian/months/august': 'ago', + 'dates/gregorian/dayperiods/am': 'AM', + 'dates/fields/month': 'mese' + } +}); + +checkDisplayNames(gDN('ar', { + style: 'long', + keys: [ + 'dates/gregorian/weekdays/thursday', + 'dates/gregorian/months/august', + 'dates/gregorian/dayperiods/am', + 'dates/fields/month', + ] +}), { + locale: 'ar', + style: 'long', + values: { + 'dates/gregorian/weekdays/thursday': 'الخميس', + 'dates/gregorian/months/august': 'أغسطس', + 'dates/gregorian/dayperiods/am': 'ص', + 'dates/fields/month': 'الشهر' + } +}); + +/* Invalid input */ + +assertThrowsInstanceOf(() => { + gDN('en-US', { + style: '', + keys: [ + 'dates/gregorian/weekdays/thursday', + ] + }); +}, RangeError); + +assertThrowsInstanceOf(() => { + gDN('en-US', { + style: 'bogus', + keys: [ + 'dates/gregorian/weekdays/thursday', + ] + }); +}, RangeError); + +assertThrowsInstanceOf(() => { + gDN('foo-X', { + keys: [ + 'dates/gregorian/weekdays/thursday', + ] + }); +}, RangeError); + +const typeErrorKeys = [ + null, + 'string', + Symbol.iterator, + 15, + 1, + 3.7, + NaN, + Infinity +]; + +for (let keys of typeErrorKeys) { + assertThrowsInstanceOf(() => { + gDN('en-US', { + keys + }); + }, TypeError); +} + +const rangeErrorKeys = [ + [''], + ['foo'], + ['dates/foo'], + ['/dates/foo'], + ['dates/foo/foo'], + ['dates/fields'], + ['dates/fields/'], + ['dates/fields/foo'], + ['dates/fields/foo/month'], + ['/dates/foo/faa/bar/baz'], + ['dates///bar/baz'], + ['dates/gregorian'], + ['dates/gregorian/'], + ['dates/gregorian/foo'], + ['dates/gregorian/months'], + ['dates/gregorian/months/foo'], + ['dates/gregorian/weekdays'], + ['dates/gregorian/weekdays/foo'], + ['dates/gregorian/dayperiods'], + ['dates/gregorian/dayperiods/foo'], + ['dates/gregorian/months/الشهر'], + [3], + [null], + ['d', 'a', 't', 'e', 's'], + ['datesEXTRA'], + ['dates/fieldsEXTRA'], + ['dates/gregorianEXTRA'], + ['dates/gregorian/monthsEXTRA'], + ['dates/gregorian/weekdaysEXTRA'], + ['dates/fields/dayperiods/amEXTRA'], + ['dates/gregori\u1161n/months/january'], + ["dates/fields/year/"], + ["dates/fields/month/"], + ["dates/fields/week/"], + ["dates/fields/day/"], + ["dates/gregorian/months/january/"], + ["dates/gregorian/weekdays/saturday/"], + ["dates/gregorian/dayperiods/am/"], + ["dates/fields/months/january/"], +]; + +for (let keys of rangeErrorKeys) { + assertThrowsInstanceOf(() => { + gDN('en-US', { + keys + }); + }, RangeError); +} + +if (typeof reportCompare === 'function') + reportCompare(0, 0); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index bf49f2268..9a8ec7679 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -2477,6 +2477,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("intl_FormatDateTime", intl_FormatDateTime, 2,0), JS_FN("intl_FormatNumber", intl_FormatNumber, 2,0), JS_FN("intl_GetCalendarInfo", intl_GetCalendarInfo, 1,0), + JS_FN("intl_ComputeDisplayNames", intl_ComputeDisplayNames, 3,0), JS_FN("intl_IsValidTimeZoneName", intl_IsValidTimeZoneName, 1,0), JS_FN("intl_NumberFormat", intl_NumberFormat, 2,0), JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0), 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/reftests/forms/input/datetime/reftest.list b/layout/reftests/forms/input/datetime/reftest.list index 0ce2002bd..a62d56c7c 100644 --- a/layout/reftests/forms/input/datetime/reftest.list +++ b/layout/reftests/forms/input/datetime/reftest.list @@ -11,3 +11,14 @@ skip-if(!Android&&!B2G&&!Mulet) == time-simple-unthemed.html time-simple-untheme # type change skip-if(Android||B2G||Mulet) == to-time-from-other-type-unthemed.html time-simple-unthemed.html skip-if(Android||B2G||Mulet) == from-time-to-other-type-unthemed.html from-time-to-other-type-unthemed-ref.html + +# content should not overflow on small width/height +skip-if(Android) == time-small-width.html time-small-width-ref.html +skip-if(Android) == time-small-height.html time-small-height-ref.html +skip-if(Android) == time-small-width-height.html time-small-width-height-ref.html + +# content (text) should be left aligned +skip-if(Android) == time-content-left-aligned.html time-content-left-aligned-ref.html + +# reset button should be right aligned +skip-if(Android) fails-if(styloVsGecko) == time-reset-button-right-aligned.html time-reset-button-right-aligned-ref.html # bug 1372062 diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html new file mode 100644 index 000000000..ad8be9adc --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-content-left-aligned-ref.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" style="width: 200px;"> + <!-- div to cover the right area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; left:40px; width:200px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-content-left-aligned.html b/layout/reftests/forms/input/datetime/time-content-left-aligned.html new file mode 100644 index 000000000..aa910cddf --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-content-left-aligned.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" style="width: 50px;"> + <!-- div to cover the right area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; left:40px; width:200px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html new file mode 100644 index 000000000..3d36f2068 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned-ref.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" value="10:00" style="float: right; color: white;"> + <!-- div to cover the left area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; right:30px; width:500px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html new file mode 100644 index 000000000..72d5cc140 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-reset-button-right-aligned.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <body> + <input type="time" value="10:00" style="width: 150px; float: right; + color: white;"> + <!-- div to cover the left area --> + <div style="display:block; position:absolute; background-color:black; + top:0px; right:30px; width:500px; height:100px;"></div> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-height-ref.html b/layout/reftests/forms/input/datetime/time-small-height-ref.html new file mode 100644 index 000000000..fcda93df9 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-height-ref.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 200px; + height: 5px; + outline: 1px dotted black; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-height.html b/layout/reftests/forms/input/datetime/time-small-height.html new file mode 100644 index 000000000..3044822fe --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-height.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 200px; + height: 5px; + outline: 1px dotted black; + color: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width-height-ref.html b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html new file mode 100644 index 000000000..0979243db --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width-height-ref.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 8px; + height: 8px; + outline: 1px dotted black; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width-height.html b/layout/reftests/forms/input/datetime/time-small-width-height.html new file mode 100644 index 000000000..a221b2819 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width-height.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 8px; + height: 8px; + outline: 1px dotted black; + color: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width-ref.html b/layout/reftests/forms/input/datetime/time-small-width-ref.html new file mode 100644 index 000000000..2379c7080 --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 10px; + height: 1.5em; + outline: 1px dotted black; + background: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input> + </body> +</html> diff --git a/layout/reftests/forms/input/datetime/time-small-width.html b/layout/reftests/forms/input/datetime/time-small-width.html new file mode 100644 index 000000000..f76f7fdfa --- /dev/null +++ b/layout/reftests/forms/input/datetime/time-small-width.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <style> +input { + width: 10px; + height: 1.5em; + outline: 1px dotted black; + color: white; + background: white; + /* Disable baseline alignment, so that our y-position isn't influenced by the + * choice of font inside of input: */ + vertical-align: top; +} + </style> + </head> + <body> + <input type="time"> + </body> +</html> diff --git a/layout/style/res/forms.css b/layout/style/res/forms.css index f045540b1..e7566e183 100644 --- a/layout/style/res/forms.css +++ b/layout/style/res/forms.css @@ -1135,3 +1135,8 @@ input[type="number"] > div > div > div:hover { /* give some indication of hover state for the up/down buttons */ background-color: lightblue; } + +input[type="date"], +input[type="time"] { + overflow: hidden !important; +} diff --git a/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/testing/marionette/interaction.js b/testing/marionette/interaction.js index c8275665d..2392485d7 100644 --- a/testing/marionette/interaction.js +++ b/testing/marionette/interaction.js @@ -76,6 +76,30 @@ const SELECTED_PROPERTY_SUPPORTED_XUL = new Set([ "TAB", ]); +/** + * Common form controls that user can change the value property interactively. + */ +const COMMON_FORM_CONTROLS = new Set([ + "input", + "textarea", + "select", +]); + +/** + * Input elements that do not fire "input" and "change" events when value + * property changes. + */ +const INPUT_TYPES_NO_EVENT = new Set([ + "checkbox", + "radio", + "file", + "hidden", + "image", + "reset", + "button", + "submit", +]); + this.interaction = {}; /** @@ -339,6 +363,32 @@ interaction.uploadFile = function (el, path) { }; /** + * Sets a form element's value. + * + * @param {DOMElement} el + * An form element, e.g. input, textarea, etc. + * @param {string} value + * The value to be set. + * + * @throws TypeError + * If |el| is not an supported form element. + */ +interaction.setFormControlValue = function* (el, value) { + if (!COMMON_FORM_CONTROLS.has(el.localName)) { + throw new TypeError("This function is for form elements only"); + } + + el.value = value; + + if (INPUT_TYPES_NO_EVENT.has(el.type)) { + return; + } + + event.input(el); + event.change(el); +}; + +/** * Send keys to element. * * @param {DOMElement|XULElement} el diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index b64eb378d..619ac249d 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -30,6 +30,7 @@ Cu.import("chrome://marionette/content/session.js"); Cu.import("chrome://marionette/content/simpletest.js"); Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -1465,6 +1466,9 @@ function* sendKeysToElement(id, val) { if (el.type == "file") { let path = val.join(""); yield interaction.uploadFile(el, path); + } else if ((el.type == "date" || el.type == "time") && + Preferences.get("dom.forms.datetime")) { + yield interaction.setFormControlValue(el, val); } else { yield interaction.sendKeysToElement( el, val, false, capabilities.get("moz:accessibilityChecks")); diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini index a8247d5a0..23bd8642c 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-checkValidity.html.ini @@ -32,25 +32,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch (in a form)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini index 223667997..5b373cfee 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini @@ -39,24 +39,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an overflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from an underflow (in a form)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] suffering from a step mismatch (in a form)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini index 6af2a360e..d8c650632 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.html.ini @@ -3,21 +3,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(with millisecond in 1 digit)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(with millisecond in 2 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(with millisecond in 3 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is greater than max(Year is 10000 should be valid)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini index 344ee0039..87f6f964e 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.html.ini @@ -3,21 +3,3 @@ [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(with millisecond in 1 digit)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(with millisecond in 2 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(with millisecond in 3 digits)] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value is less than min(Year is 10000 should be valid)] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini index 0c33bdcbe..527760e60 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-stepMismatch.html.ini @@ -2,7 +2,3 @@ type: testharness [[INPUT in DATETIME status\] The datetime type must be supported.] expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The value must mismatch the step] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini index 1cddcd033..8eb7940d9 100644 --- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini +++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-validity-valid.html.ini @@ -23,16 +23,3 @@ [[INPUT in MONTH status\] validity.valid must be false if validity.stepMismatch is true] expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] The datetime-local type must be supported.] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] validity.valid must be false if validity.rangeOverflow is true] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] validity.valid must be false if validity.rangeUnderflow is true] - expected: FAIL - - [[INPUT in DATETIME-LOCAL status\] validity.valid must be false if validity.stepMismatch is true] - expected: FAIL - diff --git a/testing/web-platform/meta/html/semantics/selectors/pseudo-classes/inrange-outofrange.html.ini b/testing/web-platform/meta/html/semantics/selectors/pseudo-classes/inrange-outofrange.html.ini deleted file mode 100644 index 5e2525ac2..000000000 --- a/testing/web-platform/meta/html/semantics/selectors/pseudo-classes/inrange-outofrange.html.ini +++ /dev/null @@ -1,20 +0,0 @@ -[inrange-outofrange.html] - type: testharness - [':in-range' matches all elements that are candidates for constraint validation, have range limitations, and that are neither suffering from an underflow nor suffering from an overflow] - expected: FAIL - - [':in-range' update number1's value < min] - expected: FAIL - - [':in-range' update number3's min < value] - expected: FAIL - - [':out-of-range' matches all elements that are candidates for constraint validation, have range limitations, and that are either suffering from an underflow or suffering from an overflow] - expected: FAIL - - [':out-of-range' update number1's value < min] - expected: FAIL - - [':out-of-range' update number3's min < value] - expected: FAIL - diff --git a/toolkit/components/mozintl/MozIntl.cpp b/toolkit/components/mozintl/MozIntl.cpp index 9c393c296..9c61c73a6 100644 --- a/toolkit/components/mozintl/MozIntl.cpp +++ b/toolkit/components/mozintl/MozIntl.cpp @@ -48,6 +48,32 @@ MozIntl::AddGetCalendarInfo(JS::Handle<JS::Value> val, JSContext* cx) return NS_OK; } +NS_IMETHODIMP +MozIntl::AddGetDisplayNames(JS::Handle<JS::Value> val, JSContext* cx) +{ + if (!val.isObject()) { + return NS_ERROR_INVALID_ARG; + } + + JS::Rooted<JSObject*> realIntlObj(cx, js::CheckedUnwrap(&val.toObject())); + if (!realIntlObj) { + return NS_ERROR_INVALID_ARG; + } + + JSAutoCompartment ac(cx, realIntlObj); + + static const JSFunctionSpec funcs[] = { + JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0), + JS_FS_END + }; + + if (!JS_DefineFunctions(cx, realIntlObj, funcs)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + NS_GENERIC_FACTORY_CONSTRUCTOR(MozIntl) NS_DEFINE_NAMED_CID(MOZ_MOZINTL_CID); diff --git a/toolkit/components/mozintl/mozIMozIntl.idl b/toolkit/components/mozintl/mozIMozIntl.idl index 67be184d4..f28824d47 100644 --- a/toolkit/components/mozintl/mozIMozIntl.idl +++ b/toolkit/components/mozintl/mozIMozIntl.idl @@ -9,4 +9,5 @@ interface mozIMozIntl : nsISupports { [implicit_jscontext] void addGetCalendarInfo(in jsval intlObject); + [implicit_jscontext] void addGetDisplayNames(in jsval intlObject); }; diff --git a/toolkit/components/mozintl/test/test_mozintl.js b/toolkit/components/mozintl/test/test_mozintl.js index 0eca2c67e..8d2720bf0 100644 --- a/toolkit/components/mozintl/test/test_mozintl.js +++ b/toolkit/components/mozintl/test/test_mozintl.js @@ -7,6 +7,7 @@ function run_test() { test_this_global(mozIntl); test_cross_global(mozIntl); + test_methods_presence(mozIntl); ok(true); } @@ -30,3 +31,16 @@ function test_cross_global(mozIntl) { equal(waivedX.getCalendarInfo() instanceof Object, false); equal(waivedX.getCalendarInfo() instanceof global.Object, true); } + +function test_methods_presence(mozIntl) { + equal(mozIntl.addGetCalendarInfo instanceof Function, true); + equal(mozIntl.addGetDisplayNames instanceof Function, true); + + let x = {}; + + mozIntl.addGetCalendarInfo(x); + equal(x.getCalendarInfo instanceof Function, true); + + mozIntl.addGetDisplayNames(x); + equal(x.getDisplayNames instanceof Function, true); +} 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..1376f70a3 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1714,6 +1714,14 @@ let DateTimePickerListener = { (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())) { return; } + + if (this._inputElement) { + // This happens when we're trying to open a picker when another picker + // is still open. We ignore this request to let the first picker + // close gracefully. + return; + } + this._inputElement = aEvent.originalTarget; this._inputElement.setDateTimePickerState(true); this.addListeners(); @@ -1728,15 +1736,17 @@ let DateTimePickerListener = { // 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, + min: this._inputElement.getMinimum(), + max: this._inputElement.getMaximum(), + step: this._inputElement.getStep(), + stepBase: this._inputElement.getStepBase(), }, }); break; } 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 851c72250..f0d4a62a4 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 @@ -80,8 +81,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/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini index 278b2ffe0..67ba2f850 100644 --- a/toolkit/content/tests/browser/browser.ini +++ b/toolkit/content/tests/browser/browser.ini @@ -26,6 +26,7 @@ skip-if = !e10s [browser_contentTitle.js] [browser_crash_previous_frameloader.js] run-if = e10s && crashreporter +[browser_datetime_datepicker.js] [browser_default_image_filename.js] [browser_f7_caret_browsing.js] [browser_findbar.js] diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js new file mode 100644 index 000000000..966a74e7a --- /dev/null +++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js @@ -0,0 +1,284 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const MONTH_YEAR = ".month-year", + DAYS_VIEW = ".days-view", + BTN_PREV_MONTH = ".prev", + BTN_NEXT_MONTH = ".next"; +const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format; + +// Create a list of abbreviations for calendar class names +const W = "weekend", + O = "outside", + S = "selection", + R = "out-of-range", + T = "today", + P = "off-step"; + +// Calendar classlist for 2016-12. Used to verify the classNames are correct. +const calendarClasslist_201612 = [ + [W, O], [O], [O], [O], [], [], [W], + [W], [], [], [], [], [], [W], + [W], [], [], [], [S], [], [W], + [W], [], [], [], [], [], [W], + [W], [], [], [], [], [], [W], + [W, O], [O], [O], [O], [O], [O], [W, O], +]; + +function getCalendarText() { + return helper.getChildren(DAYS_VIEW).map(child => child.textContent); +} + +function getCalendarClassList() { + return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList)); +} + +function mergeArrays(a, b) { + return a.map((classlist, index) => classlist.concat(b[index])); +} + +let helper = new DateTimeTestHelper(); + +registerCleanupFunction(() => { + helper.cleanup(); +}); + +/** + * Test that date picker opens to today's date when input field is blank + */ +add_task(async function test_datepicker_today() { + const date = new Date(); + + await helper.openPicker("data:text/html, <input type='date'>"); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date)); + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the correct month, with calendar days + * displayed correctly, given a date value is set. + */ +add_task(async function test_datepicker_open() { + const inputValue = "2016-12-15"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + Assert.deepEqual( + getCalendarText(), + [ + "27", "28", "29", "30", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + "11", "12", "13", "14", "15", "16", "17", + "18", "19", "20", "21", "22", "23", "24", + "25", "26", "27", "28", "29", "30", "31", + "1", "2", "3", "4", "5", "6", "7", + ], + "2016-12", + ); + Assert.deepEqual( + getCalendarClassList(), + calendarClasslist_201612, + "2016-12 classNames" + ); + + await helper.tearDown(); +}); + +/** + * When the prev month button is clicked, calendar should display the dates for + * the previous month. + */ +add_task(async function test_datepicker_prev_month_btn() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + helper.click(helper.getElement(BTN_PREV_MONTH)); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth))); + Assert.deepEqual( + getCalendarText(), + [ + "30", "31", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "10", "11", "12", + "13", "14", "15", "16", "17", "18", "19", + "20", "21", "22", "23", "24", "25", "26", + "27", "28", "29", "30", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + ], + "2016-11", + ); + + await helper.tearDown(); +}); + +/** + * When the next month button is clicked, calendar should display the dates for + * the next month. + */ +add_task(async function test_datepicker_next_month_btn() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth))); + Assert.deepEqual( + getCalendarText(), + [ + "25", "26", "27", "28", "29", "30", "31", + "1", "2", "3", "4", "5", "6", "7", + "8", "9", "10", "11", "12", "13", "14", + "15", "16", "17", "18", "19", "20", "21", + "22", "23", "24", "25", "26", "27", "28", + "29", "30", "31", "1", "2", "3", "4", + ], + "2017-01", + ); + + await helper.tearDown(); +}); + +/** + * When a date on the calendar is clicked, date picker should close and set + * value to the input box. + */ +add_task(async function test_datepicker_clicked() { + const inputValue = "2016-12-15"; + const firstDayOnCalendar = "2016-11-27"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + // Click the first item (top-left corner) of the calendar + helper.click(helper.getElement(DAYS_VIEW).children[0]); + await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() { + let inputEl = content.document.querySelector("input"); + await ContentTaskUtils.waitForEvent(inputEl, "input"); + }); + + Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar); + + await helper.tearDown(); +}); + +/** + * Make sure picker is in correct state when it is reopened. + */ +add_task(async function test_datepicker_reopen_state() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + // Navigate to the next month but does not commit the change + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth))); + EventUtils.synthesizeKey("VK_ESCAPE", {}, window); + + // Ensures the picker opens to the month of the input value + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser); + await helper.waitForPickerReady(); + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + + await helper.tearDown(); +}); + +/** + * When min and max attributes are set, calendar should show some dates as + * out-of-range. + */ +add_task(async function test_datepicker_min_max() { + const inputValue = "2016-12-15"; + const inputMin = "2016-12-05"; + const inputMax = "2016-12-25"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // R denotes out-of-range + [R], [R], [R], [R], [R], [R], [R], + [R], [], [], [], [], [], [], + [], [], [], [], [], [], [], + [], [], [], [], [], [], [], + [], [R], [R], [R], [R], [R], [R], + [R], [R], [R], [R], [R], [R], [R], + ]), + "2016-12 with min & max", + ); + + await helper.tearDown(); +}); + +/** + * When step attribute is set, calendar should show some dates as off-step. + */ +add_task(async function test_datepicker_step() { + const inputValue = "2016-12-15"; + const inputStep = "5"; + + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">`); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // P denotes off-step + [P], [P], [P], [], [P], [P], [P], + [P], [], [P], [P], [P], [P], [], + [P], [P], [P], [P], [], [P], [P], + [P], [P], [], [P], [P], [P], [P], + [], [P], [P], [P], [P], [], [P], + [P], [P], [P], [], [P], [P], [P], + ]), + "2016-12 with step", + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_min() { + const inputValue = "0001-01-01"; + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + + Assert.deepEqual( + getCalendarText(), + [ + "", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", + "14", "15", "16", "17", "18", "19", "20", + "21", "22", "23", "24", "25", "26", "27", + "28", "29", "30", "31", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + ], + "0001-01", + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_max() { + const inputValue = "275760-09-13"; + await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`); + + Assert.deepEqual( + getCalendarText(), + [ + "31", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + ], + "275760-09", + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js index 1c6c2b54f..d7ed7a9ff 100644 --- a/toolkit/content/tests/browser/head.js +++ b/toolkit/content/tests/browser/head.js @@ -31,3 +31,93 @@ function pushPrefs(...aPrefs) { SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); return deferred.promise; } + +/** + * Helper class for testing datetime input picker widget + */ +class DateTimeTestHelper { + constructor() { + this.panel = document.getElementById("DateTimePickerPanel"); + this.panel.setAttribute("animate", false); + this.tab = null; + this.frame = null; + } + + /** + * Opens a new tab with the URL of the test page, and make sure the picker is + * ready for testing. + * + * @param {String} pageUrl + */ + async openPicker(pageUrl) { + this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser); + // If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached + if (!this.panel.dateTimePopupFrame) { + await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady") + } + this.frame = this.panel.dateTimePopupFrame; + await this.waitForPickerReady(); + } + + async waitForPickerReady() { + await BrowserTestUtils.waitForEvent(this.frame, "load", true); + // Wait for picker elements to be ready + await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady"); + } + + /** + * Find an element on the picker. + * + * @param {String} selector + * @return {DOMElement} + */ + getElement(selector) { + return this.frame.contentDocument.querySelector(selector); + } + + /** + * Find the children of an element on the picker. + * + * @param {String} selector + * @return {Array<DOMElement>} + */ + getChildren(selector) { + return Array.from(this.getElement(selector).children); + } + + /** + * Click on an element + * + * @param {DOMElement} element + */ + click(element) { + EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow); + } + + /** + * Close the panel and the tab + */ + async tearDown() { + if (!this.panel.hidden) { + let pickerClosePromise = new Promise(resolve => { + this.panel.addEventListener("popuphidden", resolve, {once: true}); + }); + this.panel.hidePopup(); + this.panel.closePicker(); + await pickerClosePromise; + } + await BrowserTestUtils.removeTab(this.tab); + this.tab = null; + } + + /** + * Clean up after tests. Remove the frame to prevent leak. + */ + cleanup() { + this.frame.remove(); + this.frame = null; + this.panel.removeAttribute("animate"); + this.panel = null; + } +} diff --git a/toolkit/content/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..44ba67501 --- /dev/null +++ b/toolkit/content/widgets/calendar.js @@ -0,0 +1,171 @@ +/* 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 + * {Function} getDayString: Transform day number to string + * {Function} getWeekHeaderString: Transform day of week number to string + * {Function} setSelection: Set selection for dateKeeper + * } + * @param {Object} context + * { + * {DOMElement} weekHeader + * {DOMElement} daysView + * } + */ +function Calendar(options, context) { + const DAYS_IN_A_WEEK = 7; + + this.context = context; + this.state = { + days: [], + weekHeaders: [], + setSelection: options.setSelection, + getDayString: options.getDayString, + getWeekHeaderString: options.getWeekHeaderString + }; + 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 + * { + * {Date} dateObj + * {Number} content + * {Array<String>} classNames + * {Boolean} enabled + * } + * {Array<Object>} weekHeaders: Data for weekHeaders + * { + * {Number} content + * {Array<String>} classNames + * } + * } + */ + setProps(props) { + if (props.isVisible) { + // Transform the days and weekHeaders array for rendering + const days = props.days.map(({ dateObj, content, classNames, enabled }) => { + return { + dateObj, + textContent: this.state.getDayString(content), + className: classNames.join(" "), + enabled + }; + }); + const weekHeaders = props.weekHeaders.map(({ content, classNames }) => { + return { + textContent: this.state.getWeekHeaderString(content), + 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; + } + }, + + /** + * 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; + let targetObj = this.state.days[targetId]; + if (targetObj.enabled) { + this.state.setSelection(targetObj.dateObj); + } + } + 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..5d70416a9 --- /dev/null +++ b/toolkit/content/widgets/datekeeper.js @@ -0,0 +1,336 @@ +/* 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. + */ +function DateKeeper(props) { + this.init(props); +} + +{ + const DAYS_IN_A_WEEK = 7, + MONTHS_IN_A_YEAR = 12, + YEAR_VIEW_SIZE = 200, + YEAR_BUFFER_SIZE = 10, + // The min value is 0001-01-01 based on HTML spec: + // https://html.spec.whatwg.org/#valid-date-string + MIN_DATE = -62135596800000, + // The max value is derived from the ECMAScript spec (275760-09-13): + // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 + MAX_DATE = 8640000000000000, + MAX_YEAR = 275760, + MAX_MONTH = 9; + + DateKeeper.prototype = { + get year() { + return this.state.dateObj.getUTCFullYear(); + }, + + get month() { + return this.state.dateObj.getUTCMonth(); + }, + + get selection() { + return this.state.selection; + }, + + /** + * Initialize DateKeeper + * @param {Number} year + * @param {Number} month + * @param {Number} day + * @param {Number} min + * @param {Number} max + * @param {Number} step + * @param {Number} stepBase + * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends + * @param {Number} calViewSize + */ + init({ year, month, day, min, max, step, stepBase, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { + const today = new Date(); + + this.state = { + step, firstDayOfWeek, weekends, calViewSize, + // min & max are NaN if empty or invalid + min: new Date(Number.isNaN(min) ? MIN_DATE : min), + max: new Date(Number.isNaN(max) ? MAX_DATE : max), + stepBase: new Date(stepBase), + today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), + weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), + years: [], + dateObj: new Date(0), + selection: { year, month, day }, + }; + + this.setCalendarMonth({ + year: year === undefined ? today.getFullYear() : year, + month: month === undefined ? today.getMonth() : month + }); + }, + /** + * Set new calendar month. The year is always treated as full year, so the + * short-form is not supported. + * @param {Object} date parts + * { + * {Number} year [optional] + * {Number} month [optional] + * } + */ + setCalendarMonth({ year = this.year, month = this.month }) { + // Make sure the date is valid before setting. + // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 + if (year > MAX_YEAR || year === MAX_YEAR && month >= MAX_MONTH) { + this.state.dateObj.setUTCFullYear(MAX_YEAR, MAX_MONTH - 1, 1); + } else if (year < 1 || year === 1 && month < 0) { + this.state.dateObj.setUTCFullYear(1, 0, 1); + } else { + this.state.dateObj.setUTCFullYear(year, month, 1); + } + }, + + /** + * Set selection date + * @param {Number} year + * @param {Number} month + * @param {Number} day + */ + setSelection({ year, month, day }) { + this.state.selection.year = year; + this.state.selection.month = month; + this.state.selection.day = day; + }, + + /** + * Set month. Makes sure the day is <= the last day of the month + * @param {Number} month + */ + setMonth(month) { + this.setCalendarMonth({ year: this.year, month }); + }, + + /** + * Set year. Makes sure the day is <= the last day of the month + * @param {Number} year + */ + setYear(year) { + this.setCalendarMonth({ year, month: this.month }); + }, + + /** + * Set month by offset. Makes sure the day is <= the last day of the month + * @param {Number} offset + */ + setMonthByOffset(offset) { + this.setCalendarMonth({ year: this.year, month: this.month + offset }); + }, + + /** + * Generate the array of months + * @return {Array<Object>} + * { + * {Number} value: Month in int + * {Boolean} enabled + * } + */ + getMonths() { + 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() { + let years = []; + + const firstItem = this.state.years[0]; + const lastItem = this.state.years[this.state.years.length - 1]; + const currentYear = this.year; + + // Generate new years array when the year is outside of the first & + // last item range. If not, return the cached result. + 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++) { + const year = currentYear + i; + if (year >= 1 && year <= MAX_YEAR) { + years.push({ + value: year, + enabled: true + }); + } + } + this.state.years = years; + } + return this.state.years; + }, + + /** + * Get days for calendar + * @return {Array<Object>} + * { + * {Date} dateObj + * {Number} content + * {Array<String>} classNames + * {Boolean} enabled + * } + */ + getDays() { + const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + const month = this.month; + let days = []; + + for (let i = 0; i < this.state.calViewSize; i++) { + const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + firstDayOfMonth.getUTCDate() + i); + + let classNames = []; + let enabled = true; + + const isValid = dateObj.getTime() >= MIN_DATE && dateObj.getTime() <= MAX_DATE; + if (!isValid) { + classNames.push("out-of-range"); + enabled = false; + + days.push({ + classNames, + enabled, + }); + continue; + } + + const isWeekend = this.state.weekends.includes(dateObj.getUTCDay()); + const isCurrentMonth = month == dateObj.getUTCMonth(); + const isSelection = this.state.selection.year == dateObj.getUTCFullYear() && + this.state.selection.month == dateObj.getUTCMonth() && + this.state.selection.day == dateObj.getUTCDate(); + const isOutOfRange = dateObj.getTime() < this.state.min.getTime() || + dateObj.getTime() > this.state.max.getTime(); + const isToday = this.state.today.getTime() == dateObj.getTime(); + const isOffStep = this._checkIsOffStep(dateObj, + this._newUTCDate(dateObj.getUTCFullYear(), + dateObj.getUTCMonth(), + dateObj.getUTCDate() + 1)); + + if (isWeekend) { + classNames.push("weekend"); + } + if (!isCurrentMonth) { + classNames.push("outside"); + } + if (isSelection && !isOutOfRange && !isOffStep) { + classNames.push("selection"); + } + if (isOutOfRange) { + classNames.push("out-of-range"); + enabled = false; + } + if (isToday) { + classNames.push("today"); + } + if (isOffStep) { + classNames.push("off-step"); + enabled = false; + } + days.push({ + dateObj, + content: dateObj.getUTCDate(), + classNames, + enabled, + }); + } + return days; + }, + + /** + * Check if a date is off step given a starting point and the next increment + * @param {Date} start + * @param {Date} next + * @return {Boolean} + */ + _checkIsOffStep(start, next) { + // If the increment is larger or equal to the step, it must not be off-step. + if (next - start >= this.state.step) { + return false; + } + // Calculate the last valid date + const lastValidStep = Math.floor((next - 1 - this.state.stepBase) / this.state.step); + const lastValidTimeInMs = lastValidStep * this.state.step + this.state.stepBase.getTime(); + // The date is off-step if the last valid date is smaller than the start date + return lastValidTimeInMs < start.getTime(); + }, + + /** + * Get week headers for calendar + * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends + * @return {Array<Object>} + * { + * {Number} content + * {Array<String>} classNames + * } + */ + _getWeekHeaders(firstDayOfWeek, weekends) { + let headers = []; + let dayOfWeek = firstDayOfWeek; + + for (let i = 0; i < DAYS_IN_A_WEEK; i++) { + headers.push({ + content: dayOfWeek % DAYS_IN_A_WEEK, + classNames: 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(new Date(0).setUTCFullYear(...parts)); + }, + }; +} diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js new file mode 100644 index 000000000..0e9c9a6e6 --- /dev/null +++ b/toolkit/content/widgets/datepicker.js @@ -0,0 +1,376 @@ +/* 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] + * {Number} min + * {Number} max + * {Number} step + * {Number} stepBase + * {Number} firstDayOfWeek + * {Array<Number>} weekends + * {Array<String>} monthStrings + * {Array<String>} weekdayStrings + * {String} locale [optional]: User preferred locale + * } + */ + init(props = {}) { + this.props = props; + this._setDefaultState(); + this._createComponents(); + this._update(); + document.dispatchEvent(new CustomEvent("PickerReady")); + }, + + /* + * Set initial date picker states. + */ + _setDefaultState() { + const { year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, + monthStrings, weekdayStrings, locale } = this.props; + const dateKeeper = new DateKeeper({ + year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, + calViewSize: CAL_VIEW_SIZE + }); + + this.state = { + dateKeeper, + locale, + isMonthPickerVisible: false, + getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "", + getWeekHeaderString: weekday => weekdayStrings[weekday], + getMonthString: month => monthStrings[month], + setSelection: date => { + dateKeeper.setSelection({ + year: date.getUTCFullYear(), + month: date.getUTCMonth(), + day: date.getUTCDate(), + }); + this._update(); + this._dispatchState(); + this._closePopup(); + }, + setYear: year => { + dateKeeper.setYear(year); + dateKeeper.setSelection({ + year, + month: dateKeeper.selection.month, + day: dateKeeper.selection.day, + }); + this._update(); + this._dispatchState(); + }, + setMonth: month => { + dateKeeper.setMonth(month); + dateKeeper.setSelection({ + year: dateKeeper.selection.year, + month, + day: dateKeeper.selection.day, + }); + 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, + setSelection: this.state.setSelection, + getDayString: this.state.getDayString, + getWeekHeaderString: this.state.getWeekHeaderString + }, { + weekHeader: this.context.weekHeader, + daysView: this.context.daysView + }), + monthYear: new MonthYear({ + setYear: this.state.setYear, + setMonth: this.state.setMonth, + getMonthString: this.state.getMonthString, + locale: this.state.locale + }, { + monthYear: this.context.monthYear, + monthYearView: this.context.monthYearView + }) + }; + }, + + /** + * Update date picker and its components. + */ + _update(options = {}) { + const { dateKeeper, 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, + months: this.state.months, + years: this.state.years, + toggleMonthPicker: this.state.toggleMonthPicker, + noSmoothScroll: options.noSmoothScroll + }); + this.components.calendar.setProps({ + isVisible: !isMonthPickerVisible, + days: this.state.days, + weekHeaders: dateKeeper.state.weekHeaders + }); + + 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.selection; + // 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, + } + }, "*"); + }, + + /** + * 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; + + dateKeeper.setCalendarMonth({ + year, month + }); + dateKeeper.setSelection({ + year, month, day + }); + this._update({ noSmoothScroll: true }); + } + }; + + /** + * MonthYear is a component that handles the month & year spinners + * + * @param {Object} options + * { + * {String} locale + * {Function} setYear + * {Function} setMonth + * {Function} getMonthString + * } + * @param {DOMElement} context + */ + function MonthYear(options, context) { + const spinnerSize = 5; + const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", + timeZone: "UTC" }).format; + const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", + month: "long", + timeZone: "UTC" }).format; + this.context = context; + this.state = { dateFormat }; + this.props = {}; + this.components = { + month: new Spinner({ + setValue: month => { + this.state.isMonthSet = true; + options.setMonth(month); + }, + getDisplayString: options.getMonthString, + viewportSize: spinnerSize + }, context.monthYearView), + year: new Spinner({ + setValue: year => { + this.state.isYearSet = true; + options.setYear(year); + }, + getDisplayString: year => yearFormat(new Date(new Date(0).setUTCFullYear(year))), + viewportSize: spinnerSize + }, context.monthYearView) + }; + + this._attachEventListeners(); + } + + MonthYear.prototype = { + + /** + * Set new properties and pass them to components + * + * @param {Object} props + * { + * {Boolean} isVisible + * {Date} dateObj + * {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.dateObj.getUTCMonth(), + items: props.months, + isInfiniteScroll: true, + isValueSet: this.state.isMonthSet, + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) + }); + this.components.year.setState({ + value: props.dateObj.getUTCFullYear(), + items: props.years, + isInfiniteScroll: false, + isValueSet: this.state.isYearSet, + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) + }); + 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..ce638078f 100644 --- a/toolkit/content/widgets/datetimebox.css +++ b/toolkit/content/widgets/datetimebox.css @@ -8,9 +8,17 @@ .datetime-input-box-wrapper { -moz-appearance: none; display: inline-flex; + flex: 1; cursor: default; background-color: inherit; color: inherit; + min-width: 0; + justify-content: space-between; +} + +.datetime-input-edit-wrapper { + overflow: hidden; + white-space: nowrap; } .datetime-input { @@ -20,6 +28,8 @@ border: 0; margin: 0; ime-mode: disabled; + cursor: default; + -moz-user-select: none; } .datetime-separator { @@ -41,5 +51,5 @@ height: 12px; width: 12px; align-self: center; - justify-content: flex-end; + flex: none; } diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 05591e65a..94574038a 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -4,12 +4,466 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE bindings [ +<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd"> +%datetimeboxDTD; +]> + +<!-- +TODO +Bug 1446342: +Input type="date" not working if the other form elements has name="document" + +Any alternative solution: +document === window.document +document === this.ownerDocument +--> + <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="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[ + /* eslint-disable no-multi-spaces */ + this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[; + this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[; + this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[; + this.mSeparatorText = "/"; + /* eslint-enable no-multi-spaces */ + + this.mMinMonth = 1; + this.mMaxMonth = 12; + this.mMinDay = 1; + 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 = + window.document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + this.mDayField = + window.document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + this.mYearField = + window.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 = + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + this.mDaySeparator.textContent = this.mSeparatorText; + this.mYearSeparator = + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + this.mYearSeparator.textContent = this.mSeparatorText; + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + this.updateResetButtonVisibility(); + ]]> + </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.value) { + this.mInputElement.setUserInput(""); + } + + this.updateResetButtonVisibility(); + ]]> + </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.isAnyValueAvailable(false) && this.mInputElement.value) { + // Values in the input box was cleared, clear the input element's + // value if not empty. + this.mInputElement.setUserInput(""); + return; + } + + if (this.isFieldInvalid(this.mYearField) || + this.isFieldInvalid(this.mMonthField) || + this.isFieldInvalid(this.mDayField)) { + // 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("-"); + + if (date == this.mInputElement.value) { + return; + } + + 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); + } + + // Update input element's .value if needed. + 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 (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; + + // Home/End key does nothing on year field. + if (targetField == this.mYearField && (key == "Home" || + key == "End")) { + return; + } + + switch (key) { + case "ArrowUp": + this.incrementFieldValue(targetField, 1); + 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; + this.updateResetButtonVisibility(); + ]]> + </body> + </method> + + <method name="isAnyValueAvailable"> + <parameter name="aForPicker"/> + <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> @@ -45,13 +499,13 @@ this.mMinSecPageUpDownInterval = 10; this.mHourField = - document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-one"); this.mHourField.setAttribute("typeBuffer", ""); this.mMinuteField = - document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-two"); this.mMinuteField.setAttribute("typeBuffer", ""); this.mDayPeriodField = - document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-three"); this.mDayPeriodField.classList.remove("numeric"); this.mHourField.placeholder = this.mPlaceHolder; @@ -64,10 +518,10 @@ this.mMinuteField.setAttribute("max", this.mMaxMinute); this.mMinuteSeparator = - document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); this.mMinuteSeparator.textContent = this.mSeparatorText; this.mSpaceSeparator = - document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + window.document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); // space between time and am/pm field this.mSpaceSeparator.textContent = " "; @@ -79,6 +533,7 @@ if (this.mInputElement.value) { this.setFieldsFromInputValue(); } + this.updateResetButtonVisibility(); ]]> </constructor> @@ -138,7 +593,7 @@ } this.log("setFieldsFromInputValue: " + value); - let [hour, minute, second] = value.split(':'); + let [hour, minute, second] = value.split(":"); this.setFieldValue(this.mHourField, hour); this.setFieldValue(this.mMinuteField, minute); @@ -204,6 +659,13 @@ <method name="setInputValueFromFields"> <body> <![CDATA[ + if (!this.isAnyValueAvailable(false) && this.mInputElement.value) { + // Values in the input box was cleared, clear the input element's + // value if not empty. + this.mInputElement.setUserInput(""); + return; + } + if (this.isEmpty(this.mHourField.value) || this.isEmpty(this.mMinuteField.value) || (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) || @@ -239,6 +701,10 @@ time += "." + this.mMillisecField.value; } + if (time == this.mInputElement.value) { + return; + } + this.log("setInputValueFromFields: " + time); this.mInputElement.setUserInput(time); ]]> @@ -265,6 +731,9 @@ if (!this.isEmpty(minute)) { this.setFieldValue(this.mMinuteField, minute); } + + // Update input element's .value if needed. + this.setInputValueFromFields(); ]]> </body> </method> @@ -282,21 +751,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 && @@ -304,9 +777,11 @@ this.mDayPeriodField.value = ""; } - if (!aFromInputElement) { + if (!aFromInputElement && this.mInputElement.value) { this.mInputElement.setUserInput(""); } + + this.updateResetButtonVisibility(); ]]> </body> </method> @@ -376,6 +851,7 @@ this.mDayPeriodField.value == this.mAMIndicator ? this.mPMIndicator : this.mAMIndicator; this.mDayPeriodField.select(); + this.updateResetButtonVisibility(); this.setInputValueFromFields(); return; } @@ -433,6 +909,7 @@ this.mDayPeriodField.value = this.mPMIndicator; this.mDayPeriodField.select(); } + this.updateResetButtonVisibility(); return; } @@ -488,16 +965,30 @@ } aField.value = value; + this.updateResetButtonVisibility(); ]]> </body> </method> - <method name="isValueAvailable"> + <method name="isAnyValueAvailable"> + <parameter name="aForPicker"/> <body> <![CDATA[ + let available = !this.isEmpty(this.mHourField.value) || + !this.isEmpty(this.mMinuteField.value); + + if (available) { + return true; + } + // Picker only cares about hour:minute. - return !this.isEmpty(this.mHourField.value) || - !this.isEmpty(this.mMinuteField.value); + if (aForPicker) { + return false; + } + + return (this.mDayPeriodField && !this.isEmpty(this.mDayPeriodField.value)) || + (this.mSecondField && !this.isEmpty(this.mSecondField.value)) || + (this.mMillisecField && !this.isEmpty(this.mMillisecField.value)); ]]> </body> </method> @@ -546,7 +1037,8 @@ <content> <html:div class="datetime-input-box-wrapper" xbl:inherits="context,disabled,readonly"> - <html:span> + <html:span class="datetime-input-edit-wrapper" + anonid="edit-wrapper"> <html:input anonid="input-one" class="textbox-input datetime-input numeric" size="2" maxlength="2" @@ -563,9 +1055,8 @@ xbl:inherits="disabled,readonly,tabindex"/> </html:span> - <html:button class="datetime-reset-button" anoid="reset-button" - tabindex="-1" xbl:inherits="disabled" - onclick="document.getBindingParent(this).clearInputFields(false);"/> + <html:button class="datetime-reset-button" anonid="reset-button" + tabindex="-1" xbl:inherits="disabled"/> </html:div> </content> @@ -579,9 +1070,49 @@ this.mMax = this.mInputElement.max; this.mStep = this.mInputElement.step; this.mIsPickerOpen = false; + + this.mResetButton = + window.document.getAnonymousElementByAttribute(this, "anonid", "reset-button"); + + 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 + }); + // This is to open the picker when input element is clicked (this + // includes padding area). + this.mInputElement.addEventListener("click", this, + { mozSystemGroup: true }); ]]> </constructor> + <destructor> + <![CDATA[ + this.EVENTS.forEach((eventName) => { + this.removeEventListener(eventName, this, { mozSystemGroup: true }); + }); + this.removeEventListener("keypress", this, { + capture: true, + mozSystemGroup: true + }); + this.mInputElement.removeEventListener("click", this, + { mozSystemGroup: true }); + + this.mInputElement = null; + ]]> + </destructor> + + <property name="EVENTS" readonly="true"> + <getter> + <![CDATA[ + return ["focus", "blur", "copy", "cut", "paste", "mousedown"]; + ]]> + </getter> + </property> + <method name="log"> <parameter name="aMsg"/> <body> @@ -593,11 +1124,23 @@ </body> </method> + <method name="updateResetButtonVisibility"> + <body> + <![CDATA[ + if (this.isAnyValueAvailable(false)) { + this.mResetButton.style.visibility = "visible"; + } else { + this.mResetButton.style.visibility = "hidden"; + } + ]]> + </body> + </method> + <method name="focusInnerTextBox"> <body> <![CDATA[ this.log("focusInnerTextBox"); - document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus(); + window.document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus(); ]]> </body> </method> @@ -710,10 +1253,22 @@ </body> </method> + <method name="getCurrentValue"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="isAnyValueAvailable"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + <method name="notifyPicker"> <body> <![CDATA[ - if (this.mIsPickerOpen && this.isValueAvailable()) { + if (this.mIsPickerOpen && this.isAnyValueAvailable(true)) { this.mInputElement.updateDateTimePicker(this.getCurrentValue()); } ]]> @@ -736,72 +1291,153 @@ </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 "mousedown": { + if (aEvent.originalTarget == this.mResetButton) { + aEvent.preventDefault(); + } + 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 + + " target: " + aEvent.target); - <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, Escape or Space key. + case "Enter": + case "Escape": + case " ": { + if (this.mIsPickerOpen) { + this.mInputElement.closeDateTimePicker(); + aEvent.preventDefault(); + } + break; + } + case "Backspace": { + let targetField = aEvent.originalTarget; + targetField.value = ""; + targetField.setAttribute("typeBuffer", ""); + this.updateResetButtonVisibility(); + this.setInputValueFromFields(); + aEvent.preventDefault(); + break; + } + case "ArrowRight": + 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 + + " target: " + aEvent.target); - 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); - } + if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) { + return; + } - event.preventDefault(); - ]]> - </handler> - </handlers> + if (aEvent.originalTarget == this.mResetButton) { + this.clearInputFields(false); + } else if (!this.mIsPickerOpen) { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } + ]]> + </body> + </method> + </implementation> </binding> </bindings> diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml index 5f16f1ff0..1d6a5e772 100644 --- a/toolkit/content/widgets/datetimepicker.xml +++ b/toolkit/content/widgets/datetimepicker.xml @@ -999,13 +999,13 @@ <body> <![CDATA[ var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory"; - var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"}); + var dtfMonth = Intl.DateTimeFormat(locale, {month: "long", timeZone: "UTC"}); var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"}); var monthLabel = this.monthField.firstChild; - var tempDate = new Date(2005, 0, 1); + var tempDate = new Date(Date.UTC(2005, 0, 1)); for (var month = 0; month < 12; month++) { - tempDate.setMonth(month); + tempDate.setUTCMonth(month); monthLabel.setAttribute("value", dtfMonth.format(tempDate)); monthLabel = monthLabel.nextSibling; } diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 327f45368..b4335e1ce 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -11,17 +11,31 @@ 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> - <method name="loadPicker"> + <field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field> + <field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field> + <constructor><![CDATA[ + this.l10n = {}; + const mozIntl = Components.classes["@mozilla.org/mozintl;1"] + .getService(Components.interfaces.mozIMozIntl); + mozIntl.addGetCalendarInfo(l10n); + mozIntl.addGetDisplayNames(l10n); + // Notify DateTimePickerHelper.jsm that binding is ready. + this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady")); + ]]></constructor> + <method name="openPicker"> <parameter name="type"/> + <parameter name="anchor"/> <parameter name="detail"/> <body><![CDATA[ - this.hidden = false; this.type = type; this.pickerState = {}; // TODO: Resize picker according to content zoom level @@ -35,18 +49,28 @@ 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; + } } + this.hidden = false; + this.openPopup(anchor, "after_start", 0, 0); ]]></body> </method> <method name="closePicker"> <body><![CDATA[ - this.hidden = true; this.setInputBoxValue(true); this.pickerState = {}; this.type = undefined; this.dateTimePopupFrame.removeEventListener("load", this, true); - this.dateTimePopupFrame.contentDocument.removeEventListener("TimePickerPopupChanged", this, false); + this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false); this.dateTimePopupFrame.setAttribute("src", ""); + this.hidden = true; ]]></body> </method> <method name="setPopupValue"> @@ -55,25 +79,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 +124,56 @@ }); break; } + case "date": { + const { year, month, day } = detail.value; + const { firstDayOfWeek, weekends } = + this.getCalendarInfo(locale); + const monthStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/months/january", + "dates/gregorian/months/february", + "dates/gregorian/months/march", + "dates/gregorian/months/april", + "dates/gregorian/months/may", + "dates/gregorian/months/june", + "dates/gregorian/months/july", + "dates/gregorian/months/august", + "dates/gregorian/months/september", + "dates/gregorian/months/october", + "dates/gregorian/months/november", + "dates/gregorian/months/december", + ], "short"); + const weekdayStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/weekdays/sunday", + "dates/gregorian/weekdays/monday", + "dates/gregorian/weekdays/tuesday", + "dates/gregorian/weekdays/wednesday", + "dates/gregorian/weekdays/thursday", + "dates/gregorian/weekdays/friday", + "dates/gregorian/weekdays/saturday", + ], "short"); + + this.postMessageToPicker({ + name: "PickerInit", + detail: { + year, + // Month value from input box starts from 1 instead of 0 + month: month == undefined ? undefined : month - 1, + day, + firstDayOfWeek, + weekends, + monthStrings, + weekdayStrings, + locale, + min: detail.min, + max: detail.max, + step: detail.step, + stepBase: detail.stepBase, + } + }); + break; + } } ]]></body> </method> @@ -109,6 +197,10 @@ } break; } + case "date": { + this.sendPickerValueChanged(this.pickerState); + break; + } } ]]></body> </method> @@ -125,9 +217,60 @@ })); 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> + <method name="getCalendarInfo"> + <parameter name="locale"/> + <body><![CDATA[ + const calendarInfo = this.l10n.getCalendarInfo(locale); + + // Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday, + // so they need to be mapped to JavaScript convention with 0 as Sunday + // and 6 as Saturday + let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1, + weekendStart = calendarInfo.weekendStart - 1, + weekendEnd = calendarInfo.weekendEnd - 1; + + let weekends = []; + + // Make sure weekendEnd is greater than weekendStart + if (weekendEnd < weekendStart) { + weekendEnd += 7; + } + + // We get the weekends by incrementing weekendStart up to weekendEnd. + // If the start and end is the same day, then weekends only has one day. + for (let day = weekendStart; day <= weekendEnd; day++) { + weekends.push(day % 7); + } + + return { + firstDayOfWeek, + weekends + } + ]]></body> + </method> + <method name="getDisplayNames"> + <parameter name="locale"/> + <parameter name="keys"/> + <parameter name="style"/> + <body><![CDATA[ + const displayNames = this.l10n.getDisplayNames(locale, {keys, style}); + return keys.map(key => displayNames.values[key]); + ]]></body> + </method> <method name="handleEvent"> <parameter name="aEvent"/> <body><![CDATA[ @@ -152,11 +295,16 @@ } switch (aEvent.data.name) { - case "TimePickerPopupChanged": { + case "PickerPopupChanged": { this.pickerState = aEvent.data.detail; this.setInputBoxValue(); break; } + case "ClosePopup": { + this.hidePopup(); + this.closePicker(); + break; + } } ]]></body> </method> @@ -170,12 +318,5 @@ </method> </implementation> - <handlers> - <handler event="popuphiding"> - <![CDATA[ - this.closePicker(); - ]]> - </handler> - </handlers> </binding> </bindings> diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 208ab1931..4901320b5 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,23 +106,23 @@ 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(); + } }, /** * Whenever scroll event is detected: * - Update the index state - * - If a smooth scroll has reached its destination, set [isScrolling] state - * to false * - If the value has changed, update the [value] state and call [setValue] * - If infinite scrolling is on, reset the scrolling position if necessary */ @@ -135,14 +135,8 @@ function Spinner(props, context) { const value = itemsView[this.state.index + viewportTopOffset].value; - // Check if smooth scrolling has reached its destination. - // This prevents input box jump when input box changes values. - if (this.state.value == value && this.state.isScrolling) { - this.state.isScrolling = false; - } - - // Call setValue if value has changed, and is not smooth scrolling - if (this.state.value != value && !this.state.isScrolling) { + // Call setValue if value has changed + if (this.state.value != value) { this.state.value = value; this.props.setValue(value); } @@ -266,11 +260,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 +282,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, @@ -300,11 +291,11 @@ function Spinner(props, context) { // An "active" class is needed to simulate :active pseudo-class // because element is not focused. event.target.classList.add("active"); - this._smoothScrollToIndex(index + 1); + this._smoothScrollToIndex(index - 1); } if (event.target == down) { event.target.classList.add("active"); - this._smoothScrollToIndex(index - 1); + this._smoothScrollToIndex(index + 1); } if (event.target.parentNode == spinner) { // Listen to dragging events @@ -444,10 +435,6 @@ function Spinner(props, context) { _smoothScrollToIndex(index) { const element = this.elements.spinner.children[index]; if (element) { - // Set the isScrolling flag before smooth scrolling begins - // and remove it when it has reached the destination. - // This prevents input box jump when input box changes values - this.state.isScrolling = true; element.scrollIntoView({ behavior: "smooth", block: "start" }); diff --git a/toolkit/content/widgets/timekeeper.js b/toolkit/content/widgets/timekeeper.js index 2234c9e50..3b4e7eb0a 100644 --- a/toolkit/content/widgets/timekeeper.js +++ b/toolkit/content/widgets/timekeeper.js @@ -14,7 +14,7 @@ * { * {Date} min * {Date} max - * {Number} stepInMs + * {Number} step * {String} format: Either "12" or "24" * } */ @@ -286,15 +286,15 @@ function TimeKeeper(props) { * } */ _getSteps(startValue, endValue, minStep, formatter) { - const { min, max, stepInMs } = this.props; + const { min, max, step } = this.props; // The timeStep should be big enough so that there won't be // duplications. Ex: minimum step for minute should be 60000ms, // if smaller than that, next step might return the same minute. - const timeStep = Math.max(minStep, stepInMs); + const timeStep = Math.max(minStep, step); // Make sure the starting point and end point is not off step let time = min.valueOf() + Math.ceil((startValue - min.valueOf()) / timeStep) * timeStep; - let maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / stepInMs) * stepInMs; + let maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / step) * step; let steps = []; // Increment by timeStep until reaching the end of the range. @@ -410,9 +410,9 @@ function TimeKeeper(props) { * @return {Boolean} */ _isOffStep(time) { - const { min, stepInMs } = this.props; + const { min, step } = this.props; - return (time.valueOf() - min.valueOf()) % stepInMs != 0; + return (time.valueOf() - min.valueOf()) % step != 0; } }; } diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js index f438e9ec6..1f0463fe4 100644 --- a/toolkit/content/widgets/timepicker.js +++ b/toolkit/content/widgets/timepicker.js @@ -13,8 +13,6 @@ function TimePicker(context) { const debug = 0 ? console.log.bind(console, "[timepicker]") : function() {}; const DAY_PERIOD_IN_HOURS = 12, - SECOND_IN_MS = 1000, - MINUTE_IN_MS = 60000, DAY_IN_MS = 86400000; TimePicker.prototype = { @@ -24,9 +22,9 @@ function TimePicker(context) { * { * {Number} hour [optional]: Hour in 24 hours format (0~23), default is current hour * {Number} minute [optional]: Minute (0~59), default is current minute - * {String} min [optional]: Minimum time, in 24 hours format. ex: "05:45" - * {String} max [optional]: Maximum time, in 24 hours format. ex: "23:00" - * {Number} step [optional]: Step size in minutes. Default is 60. + * {Number} min: Minimum time, in ms + * {Number} max: Maximum time, in ms + * {Number} step: Step size in ms * {String} format [optional]: "12" for 12 hours, "24" for 24 hours format * {String} locale [optional]: User preferred locale * } @@ -51,11 +49,10 @@ function TimePicker(context) { let timerHour = hour == undefined ? now.getHours() : hour; let timerMinute = minute == undefined ? now.getMinutes() : minute; - // The spec defines 1 step == 1 second, need to convert to ms for timekeeper let timeKeeper = new TimeKeeper({ - min: this._parseTimeString(min) || new Date(0), - max: this._parseTimeString(max) || new Date(DAY_IN_MS - 1), - stepInMs: step ? step * SECOND_IN_MS : MINUTE_IN_MS, + min: new Date(Number.isNaN(min) ? 0 : min), + max: new Date(Number.isNaN(max) ? DAY_IN_MS - 1 : max), + step, format: format || "12" }); timeKeeper.setState({ hour: timerHour, minute: timerMinute }); @@ -64,17 +61,6 @@ function TimePicker(context) { }, /** - * Convert a time string from DOM attribute to a date object. - * - * @param {String} timeString: (ex. "10:30", "23:55", "12:34:56.789") - * @return {Date/Boolean} Date object or false if date is invalid. - */ - _parseTimeString(timeString) { - let time = new Date("1970-01-01T" + timeString + "Z"); - return time.toString() == "Invalid Date" ? false : time; - }, - - /** * Initalize the spinner components. */ _createComponents() { @@ -206,7 +192,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 +204,7 @@ function TimePicker(context) { }, _attachEventListeners() { window.addEventListener("message", this); + document.addEventListener("mousedown", this); }, /** @@ -231,6 +218,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 +234,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/locales/en-US/chrome/global/datetimebox.dtd b/toolkit/locales/en-US/chrome/global/datetimebox.dtd new file mode 100644 index 000000000..0deffa6b3 --- /dev/null +++ b/toolkit/locales/en-US/chrome/global/datetimebox.dtd @@ -0,0 +1,9 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!-- Placeholders for input type=date --> + +<!ENTITY date.year.placeholder "yyyy"> +<!ENTITY date.month.placeholder "mm"> +<!ENTITY date.day.placeholder "dd"> diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index e49e978f5..abc96086f 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -39,6 +39,7 @@ locale/@AB_CD@/global/customizeToolbar.dtd (%chrome/global/customizeToolbar.dtd) locale/@AB_CD@/global/customizeToolbar.properties (%chrome/global/customizeToolbar.properties) #endif + locale/@AB_CD@/global/datetimebox.dtd (%chrome/global/datetimebox.dtd) locale/@AB_CD@/global/datetimepicker.dtd (%chrome/global/datetimepicker.dtd) locale/@AB_CD@/global/dateFormat.properties (%chrome/global/dateFormat.properties) locale/@AB_CD@/global/dialogOverlay.dtd (%chrome/global/dialogOverlay.dtd) diff --git a/toolkit/modules/DateTimePickerHelper.jsm b/toolkit/modules/DateTimePickerHelper.jsm index 398687988..b509742b0 100644 --- a/toolkit/modules/DateTimePickerHelper.jsm +++ b/toolkit/modules/DateTimePickerHelper.jsm @@ -21,6 +21,7 @@ this.EXPORTED_SYMBOLS = [ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); /* * DateTimePickerHelper receives message from content side (input box) and @@ -63,9 +64,13 @@ this.DateTimePickerHelper = { return; } this.picker.closePicker(); + this.close(); break; } case "FormDateTime:UpdatePicker": { + if (!this.picker) { + return; + } this.picker.setPopupValue(aMessage.data); break; } @@ -87,6 +92,7 @@ this.DateTimePickerHelper = { if (browser) { browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed"); } + this.picker.closePicker(); this.close(); break; } @@ -97,18 +103,15 @@ 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); } }, // Get picker from browser and show it anchored to the input box. - showPicker: function(aBrowser, aData) { + showPicker: Task.async(function* (aBrowser, aData) { let rect = aData.rect; let dir = aData.dir; let type = aData.type; @@ -138,13 +141,23 @@ this.DateTimePickerHelper = { debug("aBrowser.dateTimePicker not found, exiting now."); return; } - this.picker.loadPicker(type, detail); + // The datetimepopup binding is only attached when it is needed. + // Check if openPicker method is present to determine if binding has + // been attached. If not, attach the binding first before calling it. + if (!this.picker.openPicker) { + let bindingPromise = new Promise(resolve => { + this.picker.addEventListener("DateTimePickerBindingReady", + resolve, {once: true}); + }); + this.picker.setAttribute("active", true); + yield bindingPromise; + } // The arrow panel needs an anchor to work. The popupAnchor (this._anchor) // is a transparent div that the arrow can point to. - this.picker.openPopup(this._anchor, "after_start", rect.left, rect.top); + this.picker.openPicker(type, this._anchor, detail); this.addPickerListeners(); - }, + }), // Picker is closed, do some cleanup. close: function() { diff --git a/toolkit/themes/shared/datetimeinputpickers.css b/toolkit/themes/shared/datetimeinputpickers.css new file mode 100644 index 000000000..f0c4315e5 --- /dev/null +++ b/toolkit/themes/shared/datetimeinputpickers.css @@ -0,0 +1,377 @@ +/* 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.5rem; + --date-picker-item-width: 3.3rem; + + --border: 0.1rem solid #D6D6D6; + --border-radius: 0.3rem; + --border-active-color: #B1B1B1; + + --font-color: #191919; + --fill-color: #EBEBEB; + + --today-fill-color: rgb(212, 212, 212); + + --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-header-font-color: #6C6C6C; + --weekend-header-font-color: rgb(218, 78, 68); + + --weekend-font-color: rgb(218, 78, 68); + --weekday-outside-font-color: rgb(153, 153, 153); + --weekend-outside-font-color: rgb(255, 152, 143); + + --weekday-disabled-font-color: rgba(25, 25, 25, 0.2); + --weekend-disabled-font-color: rgba(218, 78, 68, 0.2); + --disabled-fill-color: rgba(235, 235, 235, 0.8); + + --disabled-opacity: 0.2; +} + +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%; +} + +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-header-font-color); +} + +.week-header > div.weekend { + color: var(--weekend-header-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); + position: relative; + justify-content: center; + width: var(--date-picker-item-width); +} + +.days-view > .outside { + color: var(--weekday-outside-font-color); +} + +.days-view > .weekend { + color: var(--weekend-font-color); +} + +.days-view > .weekend.outside { + color: var(--weekend-outside-font-color); +} + +.days-view > .out-of-range, +.days-view > .off-step { + color: var(--weekday-disabled-font-color); + background: var(--disabled-fill-color); +} + +.days-view > .out-of-range.weekend, +.days-view > .off-step.weekend { + color: var(--weekend-disabled-font-color); +} + +.days-view > .today { + font-weight: bold; +} + +.days-view > .out-of-range::before, +.days-view > .off-step::before { + display: none; +} + +.days-view > div:hover::before, +.days-view > .select::before, +.days-view > .today::before { + top: 5%; + bottom: 5%; + left: 5%; + right: 5%; +} + +#time-picker, +.month-year-view { + display: flex; + 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::before, +.calendar-container .days-view > div::before { + position: absolute; + top: 5%; + bottom: 5%; + left: 5%; + right: 5%; + z-index: -10; + border-radius: var(--border-radius); +} + +.spinner-container > .spinner > div:hover::before, +.calendar-container .days-view > div:hover::before { + background: var(--fill-color); + border: var(--border); + content: ""; +} + +.calendar-container .days-view > div.today::before { + background: var(--today-fill-color); + content: ""; +} + +.spinner-container > .spinner:not(.scrolling) > div.selection, +.calendar-container .days-view > div.selection { + 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; + content: ""; +} + +.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 |