summaryrefslogtreecommitdiffstats
path: root/toolkit
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit')
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteController.cpp122
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp23
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h2
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteResult.idl7
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl6
-rw-r--r--toolkit/components/autocomplete/tests/unit/head_autocomplete.js5
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_hiddenResult.js76
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js71
-rw-r--r--toolkit/components/autocomplete/tests/unit/xpcshell.ini2
-rw-r--r--toolkit/components/filepicker/nsFileView.cpp7
-rw-r--r--toolkit/components/mozintl/MozIntl.cpp26
-rw-r--r--toolkit/components/mozintl/mozIMozIntl.idl1
-rw-r--r--toolkit/components/mozintl/test/test_mozintl.js14
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_search.js1
-rw-r--r--toolkit/components/places/UnifiedComplete.js2
-rw-r--r--toolkit/components/places/moz.build2
-rw-r--r--toolkit/components/places/nsNavHistory.cpp4
-rw-r--r--toolkit/components/places/nsPlacesAutoComplete.js1778
-rw-r--r--toolkit/components/places/nsPlacesAutoComplete.manifest6
-rw-r--r--toolkit/components/places/nsTaggingService.js4
-rw-r--r--toolkit/components/satchel/test/test_form_autocomplete.html12
-rw-r--r--toolkit/content/browser-content.js16
-rw-r--r--toolkit/content/customizeToolbar.js24
-rw-r--r--toolkit/content/datepicker.xhtml60
-rw-r--r--toolkit/content/jar.mn4
-rw-r--r--toolkit/content/tests/browser/browser.ini1
-rw-r--r--toolkit/content/tests/browser/browser_datetime_datepicker.js284
-rw-r--r--toolkit/content/tests/browser/head.js90
-rw-r--r--toolkit/content/timepicker.xhtml2
-rw-r--r--toolkit/content/widgets/calendar.js171
-rw-r--r--toolkit/content/widgets/datekeeper.js336
-rw-r--r--toolkit/content/widgets/datepicker.js376
-rw-r--r--toolkit/content/widgets/datetimebox.css12
-rw-r--r--toolkit/content/widgets/datetimebox.xml782
-rw-r--r--toolkit/content/widgets/datetimepicker.xml6
-rw-r--r--toolkit/content/widgets/datetimepopup.xml171
-rw-r--r--toolkit/content/widgets/spinner.js47
-rw-r--r--toolkit/content/widgets/timekeeper.js12
-rw-r--r--toolkit/content/widgets/timepicker.js39
-rw-r--r--toolkit/content/widgets/toolbar.xml53
-rw-r--r--toolkit/locales/en-US/chrome/global/autocomplete.properties5
-rw-r--r--toolkit/locales/en-US/chrome/global/customizeToolbar.properties1
-rw-r--r--toolkit/locales/en-US/chrome/global/datetimebox.dtd9
-rw-r--r--toolkit/locales/jar.mn1
-rw-r--r--toolkit/modules/DateTimePickerHelper.jsm29
-rw-r--r--toolkit/mozapps/extensions/AddonManager.jsm6
-rw-r--r--toolkit/mozapps/extensions/DeferredSave.jsm6
-rw-r--r--toolkit/mozapps/extensions/addonManager.js4
-rw-r--r--toolkit/mozapps/extensions/amInstallTrigger.js2
-rw-r--r--toolkit/mozapps/extensions/amWebInstallListener.js2
-rw-r--r--toolkit/mozapps/extensions/content/extensions.js2
-rw-r--r--toolkit/mozapps/extensions/content/update.js4
-rw-r--r--toolkit/mozapps/extensions/internal/AddonRepository.jsm4
-rw-r--r--toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm2
-rw-r--r--toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm2
-rw-r--r--toolkit/mozapps/extensions/internal/Content.js4
-rw-r--r--toolkit/mozapps/extensions/internal/GMPProvider.jsm8
-rw-r--r--toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm8
-rw-r--r--toolkit/mozapps/extensions/internal/PluginProvider.jsm2
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProvider.jsm6
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProviderUtils.js2
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser-common.ini2
-rw-r--r--toolkit/mozapps/installer/find-dupes.py115
-rw-r--r--toolkit/mozapps/installer/packager.mk2
-rw-r--r--toolkit/mozapps/webextensions/test/browser/browser-common.ini1
-rw-r--r--toolkit/themes/shared/datetimeinputpickers.css377
-rw-r--r--toolkit/themes/shared/datetimepopup.css11
-rw-r--r--toolkit/themes/shared/icons/calendar-arrows.svg13
-rw-r--r--toolkit/themes/shared/icons/spinner-arrows.svg13
-rw-r--r--toolkit/themes/shared/jar.inc.mn5
-rw-r--r--toolkit/themes/shared/timepicker.css153
71 files changed, 4934 insertions, 524 deletions
diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
index 5d69ea1a3..9ca382fe5 100644
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -1637,58 +1637,72 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
MOZ_ASSERT(mResults.Count() >= aSearchIndex + 1,
"aSearchIndex should always be valid for mResults");
- uint32_t oldRowCount = mRowCount;
- // If the search failed, increase the match count to include the error
- // description.
- if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
- nsAutoString error;
- aResult->GetErrorDescription(error);
- if (!error.IsEmpty()) {
- ++mRowCount;
- if (mTree) {
- mTree->RowCountChanged(oldRowCount, 1);
+ bool isTypeAheadResult = false;
+ aResult->GetTypeAheadResult(&isTypeAheadResult);
+
+ if (!isTypeAheadResult) {
+ uint32_t oldRowCount = mRowCount;
+ // If the search failed, increase the match count to include the error
+ // description.
+ if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
+ nsAutoString error;
+ aResult->GetErrorDescription(error);
+ if (!error.IsEmpty()) {
+ ++mRowCount;
+ if (mTree) {
+ mTree->RowCountChanged(oldRowCount, 1);
+ }
}
- }
- } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
- searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
- // Increase the match count for all matches in this result.
- uint32_t totalMatchCount = 0;
- for (uint32_t i = 0; i < mResults.Length(); i++) {
- nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
- if (result) {
- uint32_t matchCount = 0;
- result->GetMatchCount(&matchCount);
- totalMatchCount += matchCount;
+ } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+ searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+ // Increase the match count for all matches in this result.
+ uint32_t totalMatchCount = 0;
+ for (uint32_t i = 0; i < mResults.Length(); i++) {
+ nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
+ if (result) {
+ // not all results implement this, so it can likely fail.
+ bool typeAhead = false;
+ result->GetTypeAheadResult(&typeAhead);
+ if (!typeAhead) {
+ uint32_t matchCount = 0;
+ result->GetMatchCount(&matchCount);
+ totalMatchCount += matchCount;
+ }
+ }
}
- }
- uint32_t delta = totalMatchCount - oldRowCount;
+ uint32_t delta = totalMatchCount - oldRowCount;
- mRowCount += delta;
- if (mTree) {
- mTree->RowCountChanged(oldRowCount, delta);
+ mRowCount += delta;
+ if (mTree) {
+ mTree->RowCountChanged(oldRowCount, delta);
+ }
}
- }
- // Try to autocomplete the default index for this search.
- // Do this before invalidating so the binding knows about it.
- CompleteDefaultIndex(aSearchIndex);
+ // Try to autocomplete the default index for this search.
+ // Do this before invalidating so the binding knows about it.
+ CompleteDefaultIndex(aSearchIndex);
- // Refresh the popup view to display the new search results
- nsCOMPtr<nsIAutoCompletePopup> popup;
- input->GetPopup(getter_AddRefs(popup));
- NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
- popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);
+ // Refresh the popup view to display the new search results
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
+ popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);
- uint32_t minResults;
- input->GetMinResultsForPopup(&minResults);
+ uint32_t minResults;
+ input->GetMinResultsForPopup(&minResults);
- // Make sure the popup is open, if necessary, since we now have at least one
- // search result ready to display. Don't force the popup closed if we might
- // get results in the future to avoid unnecessarily canceling searches.
- if (mRowCount || !minResults) {
- OpenPopup();
- } else if (mSearchesOngoing == 0) {
- ClosePopup();
+ // Make sure the popup is open, if necessary, since we now have at least one
+ // search result ready to display. Don't force the popup closed if we might
+ // get results in the future to avoid unnecessarily canceling searches.
+ if (mRowCount || !minResults) {
+ OpenPopup();
+ } else if (mSearchesOngoing == 0) {
+ ClosePopup();
+ }
+ } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+ searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+ // Try to autocomplete the default index for this search.
+ CompleteDefaultIndex(aSearchIndex);
}
return NS_OK;
@@ -2033,14 +2047,20 @@ nsAutoCompleteController::RowIndexToSearch(int32_t aRowIndex, int32_t *aSearchIn
uint32_t rowCount = 0;
- uint16_t searchResult;
- result->GetSearchResult(&searchResult);
+ // Skip past the result completely if it is marked as hidden
+ bool isTypeAheadResult = false;
+ result->GetTypeAheadResult(&isTypeAheadResult);
- // Find out how many results were provided by the
- // current nsIAutoCompleteSearch.
- if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
- searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
- result->GetMatchCount(&rowCount);
+ if (!isTypeAheadResult) {
+ uint16_t searchResult;
+ result->GetSearchResult(&searchResult);
+
+ // Find out how many results were provided by the
+ // current nsIAutoCompleteSearch.
+ if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+ searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+ result->GetMatchCount(&rowCount);
+ }
}
// If the given row index is within the results range
diff --git a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp
index 9fd2c0022..683ac462a 100644
--- a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp
@@ -43,7 +43,8 @@ struct AutoCompleteSimpleResultMatch
nsAutoCompleteSimpleResult::nsAutoCompleteSimpleResult() :
mDefaultIndex(-1),
- mSearchResult(RESULT_NOMATCH)
+ mSearchResult(RESULT_NOMATCH),
+ mTypeAheadResult(false)
{
}
@@ -66,6 +67,12 @@ nsAutoCompleteSimpleResult::AppendResult(nsIAutoCompleteResult* aResult)
mErrorDescription = errorDescription;
}
+ bool typeAheadResult = false;
+ if (NS_SUCCEEDED(aResult->GetTypeAheadResult(&typeAheadResult)) &&
+ typeAheadResult) {
+ mTypeAheadResult = typeAheadResult;
+ }
+
int32_t defaultIndex = -1;
if (NS_SUCCEEDED(aResult->GetDefaultIndex(&defaultIndex)) &&
defaultIndex >= 0) {
@@ -166,6 +173,20 @@ nsAutoCompleteSimpleResult::SetErrorDescription(
return NS_OK;
}
+// typeAheadResult
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetTypeAheadResult(bool *aTypeAheadResult)
+{
+ *aTypeAheadResult = mTypeAheadResult;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::SetTypeAheadResult(bool aTypeAheadResult)
+{
+ mTypeAheadResult = aTypeAheadResult;
+ return NS_OK;
+}
+
NS_IMETHODIMP
nsAutoCompleteSimpleResult::InsertMatchAt(int32_t aIndex,
const nsAString& aValue,
diff --git a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h
index 28968aa57..61ee542e4 100644
--- a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h
+++ b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h
@@ -38,6 +38,8 @@ protected:
int32_t mDefaultIndex;
uint32_t mSearchResult;
+ bool mTypeAheadResult;
+
nsCOMPtr<nsIAutoCompleteSimpleResultListener> mListener;
};
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl
index c719d9427..9ae22ade7 100644
--- a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl
@@ -50,6 +50,13 @@ interface nsIAutoCompleteResult : nsISupports
readonly attribute unsigned long matchCount;
/**
+ * If true, the results will not be displayed in the popup. However,
+ * if a default index is specified, the default item will still be
+ * completed in the input.
+ */
+ readonly attribute boolean typeAheadResult;
+
+ /**
* Get the value of the result at the given index
*/
AString getValueAt(in long index);
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl
index 5e92e037a..6a8827ab8 100644
--- a/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl
@@ -42,6 +42,12 @@ interface nsIAutoCompleteSimpleResult : nsIAutoCompleteResult
void setSearchResult(in unsigned short aSearchResult);
/**
+ * A writer for the readonly attribute 'typeAheadResult', typically set
+ * because a result is only intended for type-ahead completion.
+ */
+ void setTypeAheadResult(in boolean aHidden);
+
+ /**
* Inserts a match consisting of the given value, comment, image, style and
* the value to use for defaultIndex completion at a given position.
* @param aIndex
diff --git a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js
index 1443879f0..5a458bdf4 100644
--- a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js
+++ b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js
@@ -85,6 +85,11 @@ AutoCompleteResultBase.prototype = {
defaultIndex: -1,
+ _typeAheadResult: false,
+ get typeAheadResult() {
+ return this._typeAheadResult;
+ },
+
get matchCount() {
return this._values.length;
},
diff --git a/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js b/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
new file mode 100644
index 000000000..8e2485716
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function AutoCompleteResult(aValues) {
+ this._values = aValues;
+ this.defaultIndex = -1;
+ this._typeAheadResult = false;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteTypeAheadResult(aValues) {
+ this._values = aValues;
+ this.defaultIndex = 0;
+ this._typeAheadResult = true;
+}
+AutoCompleteTypeAheadResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+
+/**
+ * Test AutoComplete with multiple AutoCompleteSearch sources, with one of them
+ * being hidden from the popup, but can still do typeahead completion.
+ */
+function run_test() {
+ do_test_pending();
+
+ var inputStr = "moz";
+
+ // Type ahead result
+ var searchTypeAhead = new AutoCompleteSearchBase("search1",
+ new AutoCompleteTypeAheadResult(["mozillaTest1"]));
+ // Regular result
+ var searchNormal = new AutoCompleteSearchBase("search2",
+ new AutoCompleteResult(["mozillaTest2"]));
+
+ // Register searches so AutoCompleteController can find them
+ registerAutoCompleteSearch(searchNormal);
+ registerAutoCompleteSearch(searchTypeAhead);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete.
+ var input = new AutoCompleteInputBase([searchTypeAhead.name, searchNormal.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = inputStr;
+
+ // Caret must be at the end. Autofill doesn't happen unless you're typing
+ // characters at the end.
+ var strLen = inputStr.length;
+ input.selectTextRange(strLen, strLen);
+ do_check_eq(input.selectionStart, strLen);
+ do_check_eq(input.selectionEnd, strLen);
+
+ var controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ controller.input = input;
+ controller.startSearch(inputStr);
+
+ input.onSearchComplete = function() {
+ // Hidden results should still be able to do inline autocomplete
+ do_check_eq(input.textValue, "mozillaTest1");
+
+ // Now, let's fill the textbox with the first result of the popup.
+ // The first search is marked as hidden, so we must always get the
+ // second search.
+ controller.handleEnter(true);
+ do_check_eq(input.textValue, "mozillaTest2");
+
+ // Only one item in the popup.
+ do_check_eq(controller.matchCount, 1);
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(searchNormal);
+ unregisterAutoCompleteSearch(searchTypeAhead);
+ do_test_finished();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js b/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
new file mode 100644
index 000000000..fb4153355
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
@@ -0,0 +1,71 @@
+/* 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/. */
+
+function AutoCompleteTypeAheadResult(aValues, aFinalCompleteValues) {
+ this._values = aValues;
+ this._finalCompleteValues = aFinalCompleteValues;
+ this.defaultIndex = 0;
+ this._typeAheadResult = true;
+}
+AutoCompleteTypeAheadResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteResult(aValues) {
+ this._values = aValues;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popupOpen = true;
+ this.completeDefaultIndex = true;
+ this.completeSelectedIndex = true;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_handleEnter() {
+ doSearch("moz", function(aController) {
+ do_check_eq(aController.input.textValue, "mozilla.com");
+ aController.handleEnter(true);
+ do_check_eq(aController.input.textValue, "mozilla.org");
+ });
+});
+
+function doSearch(aSearchString, aOnCompleteCallback) {
+ let typeAheadSearch = new AutoCompleteSearchBase(
+ "typeAheadSearch",
+ new AutoCompleteTypeAheadResult([ "mozilla.com" ], [ "http://www.mozilla.com" ])
+ );
+ registerAutoCompleteSearch(typeAheadSearch);
+
+ let search = new AutoCompleteSearchBase(
+ "search",
+ new AutoCompleteResult([ "mozilla.org" ])
+ );
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput([ typeAheadSearch.name, search.name ]);
+ input.textValue = aSearchString;
+
+ // Caret must be at the end for autofill to happen.
+ let strLen = aSearchString.length;
+ input.selectTextRange(strLen, strLen);
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ // Clean up.
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/xpcshell.ini b/toolkit/components/autocomplete/tests/unit/xpcshell.ini
index 4d193965c..daf89db17 100644
--- a/toolkit/components/autocomplete/tests/unit/xpcshell.ini
+++ b/toolkit/components/autocomplete/tests/unit/xpcshell.ini
@@ -18,7 +18,9 @@ tail =
[test_finalCompleteValue_forceComplete.js]
[test_finalCompleteValueSelectedIndex.js]
[test_finalDefaultCompleteValue.js]
+[test_hiddenResult.js]
[test_immediate_search.js]
[test_insertMatchAt.js]
+[test_popupSelectionVsDefaultCompleteValue.js]
[test_previousResult.js]
[test_stopSearch.js]
diff --git a/toolkit/components/filepicker/nsFileView.cpp b/toolkit/components/filepicker/nsFileView.cpp
index ad4471e86..9a8278496 100644
--- a/toolkit/components/filepicker/nsFileView.cpp
+++ b/toolkit/components/filepicker/nsFileView.cpp
@@ -133,6 +133,13 @@ NS_IMETHODIMP nsFileResult::GetMatchCount(uint32_t *aMatchCount)
return NS_OK;
}
+NS_IMETHODIMP nsFileResult::GetTypeAheadResult(bool *aTypeAheadResult)
+{
+ NS_ENSURE_ARG_POINTER(aTypeAheadResult);
+ *aTypeAheadResult = false;
+ return NS_OK;
+}
+
NS_IMETHODIMP nsFileResult::GetValueAt(int32_t index, nsAString & aValue)
{
aValue = mValues[index];
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/passwordmgr/test/unit/test_logins_search.js b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
index 188c75039..730771981 100644
--- a/toolkit/components/passwordmgr/test/unit/test_logins_search.js
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
@@ -192,7 +192,6 @@ add_task(function test_search_all_full_case_sensitive()
{
checkAllSearches({ hostname: "http://www.example.com" }, 1);
checkAllSearches({ hostname: "http://www.example.com/" }, 0);
- checkAllSearches({ hostname: "http://" }, 0);
checkAllSearches({ hostname: "example.com" }, 0);
checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js
index ad3d35aab..acd358b11 100644
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -1245,7 +1245,7 @@ Search.prototype = {
// * If the protocol differs we should not match. For example if the user
// searched https we should not return http.
try {
- let prefixURI = NetUtil.newURI(this._strippedPrefix);
+ let prefixURI = NetUtil.newURI(this._strippedPrefix + match.token);
let finalURI = NetUtil.newURI(match.url);
if (prefixURI.scheme != finalURI.scheme)
return false;
diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build
index adac79cba..85e1e93e1 100644
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -78,6 +78,8 @@ if CONFIG['MOZ_PLACES']:
EXTRA_COMPONENTS += [
'ColorAnalyzer.js',
'nsLivemarkService.js',
+ 'nsPlacesAutoComplete.js',
+ 'nsPlacesAutoComplete.manifest',
'nsPlacesExpiration.js',
'nsTaggingService.js',
'PageIconProtocolHandler.js',
diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp
index 8cf3a2e32..7f4007c1a 100644
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -949,6 +949,10 @@ nsresult // static
nsNavHistory::AsciiHostNameFromHostString(const nsACString& aHostName,
nsACString& aAscii)
{
+ aAscii.Truncate();
+ if (aHostName.IsEmpty()) {
+ return NS_OK;
+ }
// To properly generate a uri we must provide a protocol.
nsAutoCString fakeURL("http://");
fakeURL.Append(aHostName);
diff --git a/toolkit/components/places/nsPlacesAutoComplete.js b/toolkit/components/places/nsPlacesAutoComplete.js
new file mode 100644
index 000000000..29bdae4c1
--- /dev/null
+++ b/toolkit/components/places/nsPlacesAutoComplete.js
@@ -0,0 +1,1778 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+// This SQL query fragment provides the following:
+// - whether the entry is bookmarked (kQueryIndexBookmarked)
+// - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle)
+// - the tags associated with a bookmarked entry (kQueryIndexTags)
+const kBookTagSQLFragment =
+ `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,
+ (
+ SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL
+ ORDER BY lastModified DESC LIMIT 1
+ ) AS btitle,
+ (
+ SELECT GROUP_CONCAT(t.title, ',')
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent
+ WHERE b.fk = h.id
+ ) AS tags`;
+
+// observer topics
+const kTopicShutdown = "places-shutdown";
+const kPrefChanged = "nsPref:changed";
+
+// Match type constants. These indicate what type of search function we should
+// be using.
+const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
+const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
+const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
+const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
+const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
+
+// AutoComplete index constants. All AutoComplete queries will provide these
+// columns in this order.
+const kQueryIndexURL = 0;
+const kQueryIndexTitle = 1;
+const kQueryIndexFaviconURL = 2;
+const kQueryIndexBookmarked = 3;
+const kQueryIndexBookmarkTitle = 4;
+const kQueryIndexTags = 5;
+const kQueryIndexVisitCount = 6;
+const kQueryIndexTyped = 7;
+const kQueryIndexPlaceId = 8;
+const kQueryIndexQueryType = 9;
+const kQueryIndexOpenPageCount = 10;
+
+// AutoComplete query type constants. Describes the various types of queries
+// that we can process.
+const kQueryTypeKeyword = 0;
+const kQueryTypeFiltered = 1;
+
+// This separator is used as an RTL-friendly way to split the title and tags.
+// It can also be used by an nsIAutoCompleteResult consumer to re-split the
+// "comment" back into the title and the tag.
+const kTitleTagsSeparator = " \u2013 ";
+
+const kBrowserUrlbarBranch = "browser.urlbar.";
+// Toggle autocomplete.
+const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled";
+// Toggle autoFill.
+const kBrowserUrlbarAutofillPref = "autoFill";
+// Whether to search only typed entries.
+const kBrowserUrlbarAutofillTypedPref = "autoFill.typed";
+
+// The Telemetry histogram for urlInlineComplete query on domain
+const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService",
+ "@mozilla.org/intl/texttosuburi;1",
+ "nsITextToSubURI");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helpers
+
+/**
+ * Initializes our temporary table on a given database.
+ *
+ * @param aDatabase
+ * The mozIStorageConnection to set up the temp table on.
+ */
+function initTempTable(aDatabase)
+{
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTables.h.
+ let stmt = aDatabase.createAsyncStatement(
+ `CREATE TEMP TABLE moz_openpages_temp (
+ url TEXT PRIMARY KEY
+ , open_count INTEGER
+ )`
+ );
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTriggers.h.
+ stmt = aDatabase.createAsyncStatement(
+ `CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger
+ AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
+ WHEN NEW.open_count = 0
+ BEGIN
+ DELETE FROM moz_openpages_temp
+ WHERE url = NEW.url;
+ END`
+ );
+ stmt.executeAsync();
+ stmt.finalize();
+}
+
+/**
+ * Used to unescape encoded URI strings, and drop information that we do not
+ * care about for searching.
+ *
+ * @param aURIString
+ * The text to unescape and modify.
+ * @return the modified uri.
+ */
+function fixupSearchText(aURIString)
+{
+ let uri = stripPrefix(aURIString);
+ return gTextURIService.unEscapeURIForUI("UTF-8", uri);
+}
+
+/**
+ * Strip prefixes from the URI that we don't care about for searching.
+ *
+ * @param aURIString
+ * The text to modify.
+ * @return the modified uri.
+ */
+function stripPrefix(aURIString)
+{
+ let uri = aURIString;
+
+ if (uri.indexOf("http://") == 0) {
+ uri = uri.slice(7);
+ }
+ else if (uri.indexOf("https://") == 0) {
+ uri = uri.slice(8);
+ }
+ else if (uri.indexOf("ftp://") == 0) {
+ uri = uri.slice(6);
+ }
+
+ if (uri.indexOf("www.") == 0) {
+ uri = uri.slice(4);
+ }
+ return uri;
+}
+
+/**
+ * safePrefGetter get the pref with type safety.
+ * This will return the default value provided if no pref is set.
+ *
+ * @param aPrefBranch
+ * The nsIPrefBranch containing the required preference
+ * @param aName
+ * A preference name
+ * @param aDefault
+ * The preference's default value
+ * @return the preference value or provided default
+ */
+
+function safePrefGetter(aPrefBranch, aName, aDefault) {
+ let types = {
+ boolean: "Bool",
+ number: "Int",
+ string: "Char"
+ };
+ let type = types[typeof(aDefault)];
+ if (!type) {
+ throw "Unknown type!";
+ }
+
+ // If the pref isn't set, we want to use the default.
+ if (aPrefBranch.getPrefType(aName) == Ci.nsIPrefBranch.PREF_INVALID) {
+ return aDefault;
+ }
+ try {
+ return aPrefBranch["get" + type + "Pref"](aName);
+ }
+ catch (e) {
+ return aDefault;
+ }
+}
+
+/**
+ * Whether UnifiedComplete is alive.
+ */
+function isUnifiedCompleteInstantiated() {
+ try {
+ return Components.manager.QueryInterface(Ci.nsIServiceManager)
+ .isServiceInstantiated(Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"],
+ Ci.mozIPlacesAutoComplete);
+ } catch (ex) {
+ return false;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AutoCompleteStatementCallbackWrapper class
+
+/**
+ * Wraps a callback and ensures that handleCompletion is not dispatched if the
+ * query is no longer tracked.
+ *
+ * @param aAutocomplete
+ * A reference to a nsPlacesAutoComplete.
+ * @param aCallback
+ * A reference to a mozIStorageStatementCallback
+ * @param aDBConnection
+ * The database connection to execute the queries on.
+ */
+function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback,
+ aDBConnection)
+{
+ this._autocomplete = aAutocomplete;
+ this._callback = aCallback;
+ this._db = aDBConnection;
+}
+
+AutoCompleteStatementCallbackWrapper.prototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIStorageStatementCallback
+
+ handleResult: function ACSCW_handleResult(aResultSet)
+ {
+ this._callback.handleResult.apply(this._callback, arguments);
+ },
+
+ handleError: function ACSCW_handleError(aError)
+ {
+ this._callback.handleError.apply(this._callback, arguments);
+ },
+
+ handleCompletion: function ACSCW_handleCompletion(aReason)
+ {
+ // Only dispatch handleCompletion if we are not done searching and are a
+ // pending search.
+ if (!this._autocomplete.isSearchComplete() &&
+ this._autocomplete.isPendingSearch(this._handle)) {
+ this._callback.handleCompletion.apply(this._callback, arguments);
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// AutoCompleteStatementCallbackWrapper
+
+ /**
+ * Executes the specified query asynchronously. This object will notify
+ * this._callback if we should notify (logic explained in handleCompletion).
+ *
+ * @param aQueries
+ * The queries to execute asynchronously.
+ * @return a mozIStoragePendingStatement that can be used to cancel the
+ * queries.
+ */
+ executeAsync: function ACSCW_executeAsync(aQueries)
+ {
+ return this._handle = this._db.executeAsync(aQueries, aQueries.length,
+ this);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.mozIStorageStatementCallback,
+ ])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsPlacesAutoComplete class
+//// @mozilla.org/autocomplete/search;1?name=history
+
+function nsPlacesAutoComplete()
+{
+ //////////////////////////////////////////////////////////////////////////////
+ //// Shared Constants for Smart Getters
+
+ // TODO bug 412736 in case of a frecency tie, break it with h.typed and
+ // h.visit_count which is better than nothing. This is slow, so not doing it
+ // yet...
+ function baseQuery(conditions = "") {
+ let query = `SELECT h.url, h.title, f.url, ${kBookTagSQLFragment},
+ h.visit_count, h.typed, h.id, :query_type,
+ t.open_count
+ FROM moz_places h
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t ON t.url = h.url
+ WHERE h.frecency <> 0
+ AND AUTOCOMPLETE_MATCH(:searchString, h.url,
+ IFNULL(btitle, h.title), tags,
+ h.visit_count, h.typed,
+ bookmarked, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ${conditions}
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT :maxResults`;
+ return query;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Smart Getters
+
+ XPCOMUtils.defineLazyGetter(this, "_db", function() {
+ // Get a cloned, read-only version of the database. We'll only ever write
+ // to our own in-memory temp table, and having a cloned copy means we do not
+ // run the risk of our queries taking longer due to the main database
+ // connection performing a long-running task.
+ let db = PlacesUtils.history.DBConnection.clone(true);
+
+ // Autocomplete often fallbacks to a table scan due to lack of text indices.
+ // In such cases a larger cache helps reducing IO. The default Storage
+ // value is MAX_CACHE_SIZE_BYTES in storage/mozStorageConnection.cpp.
+ let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Create our in-memory tables for tab tracking.
+ initTempTable(db);
+
+ // Populate the table with current open pages cache contents.
+ if (this._openPagesCache.length > 0) {
+ // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter.
+ let stmt = this._registerOpenPageQuery =
+ db.createAsyncStatement(this._registerOpenPageQuerySQL);
+ let params = stmt.newBindingParamsArray();
+ for (let i = 0; i < this._openPagesCache.length; i++) {
+ let bp = params.newBindingParams();
+ bp.bindByName("page_url", this._openPagesCache[i]);
+ params.addParams(bp);
+ }
+ stmt.bindParameters(params);
+ stmt.executeAsync();
+ stmt.finalize();
+ delete this._openPagesCache;
+ }
+
+ return db;
+ });
+
+ this._customQuery = (conditions = "") => {
+ return this._db.createAsyncStatement(baseQuery(conditions));
+ };
+
+ XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() {
+ return this._db.createAsyncStatement(baseQuery());
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() {
+ // Enforce ignoring the visit_count index, since the frecency one is much
+ // faster in this case. ANALYZE helps the query planner to figure out the
+ // faster path, but it may not have run yet.
+ return this._db.createAsyncStatement(baseQuery("AND +h.visit_count > 0"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() {
+ return this._db.createAsyncStatement(baseQuery("AND bookmarked"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() {
+ return this._db.createAsyncStatement(baseQuery("AND tags IS NOT NULL"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() {
+ return this._db.createAsyncStatement(
+ `SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ :query_type, t.open_count, NULL
+ FROM moz_openpages_temp t
+ LEFT JOIN moz_places h ON h.url = t.url
+ WHERE h.id IS NULL
+ AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
+ NULL, NULL, NULL, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY t.ROWID DESC
+ LIMIT :maxResults`
+ );
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() {
+ return this._db.createAsyncStatement(baseQuery("AND h.typed = 1"));
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() {
+ return this._db.createAsyncStatement(
+ `/* do not warn (bug 487789) */
+ SELECT h.url, h.title, f.url, ${kBookTagSQLFragment},
+ h.visit_count, h.typed, h.id, :query_type, t.open_count
+ FROM (
+ SELECT ROUND(
+ MAX(use_count) * (1 + (input = :search_string)), 1
+ ) AS rank, place_id
+ FROM moz_inputhistory
+ WHERE input BETWEEN :search_string AND :search_string || X'FFFF'
+ GROUP BY place_id
+ ) AS i
+ JOIN moz_places h ON h.id = i.place_id
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t ON t.url = h.url
+ WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
+ IFNULL(btitle, h.title), tags,
+ h.visit_count, h.typed, bookmarked,
+ t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY rank DESC, h.frecency DESC`
+ );
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() {
+ return this._db.createAsyncStatement(
+ `/* do not warn (bug 487787) */
+ SELECT REPLACE(h.url, '%s', :query_string) AS search_url, h.title,
+ IFNULL(f.url, (SELECT f.url
+ FROM moz_places
+ JOIN moz_favicons f ON f.id = favicon_id
+ WHERE rev_host = h.rev_host
+ ORDER BY frecency DESC
+ LIMIT 1)
+ ), 1, NULL, NULL, h.visit_count, h.typed, h.id,
+ :query_type, t.open_count
+ FROM moz_keywords k
+ JOIN moz_places h ON k.place_id = h.id
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t ON t.url = search_url
+ WHERE k.keyword = LOWER(:keyword)`
+ );
+ });
+
+ this._registerOpenPageQuerySQL =
+ `INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)
+ VALUES (:page_url,
+ IFNULL(
+ (
+ SELECT open_count + 1
+ FROM moz_openpages_temp
+ WHERE url = :page_url
+ ),
+ 1
+ )
+ )`;
+ XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() {
+ return this._db.createAsyncStatement(this._registerOpenPageQuerySQL);
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() {
+ return this._db.createAsyncStatement(
+ `UPDATE moz_openpages_temp
+ SET open_count = open_count - 1
+ WHERE url = :page_url`
+ );
+ });
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ // load preferences
+ this._prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ getBranch(kBrowserUrlbarBranch);
+ this._syncEnabledPref();
+ this._loadPrefs(true);
+
+ // register observers
+ this._os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ this._os.addObserver(this, kTopicShutdown, false);
+
+}
+
+nsPlacesAutoComplete.prototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAutoCompleteSearch
+
+ startSearch: function PAC_startSearch(aSearchString, aSearchParam,
+ aPreviousResult, aListener)
+ {
+ // Stop the search in case the controller has not taken care of it.
+ this.stopSearch();
+
+ // Note: We don't use aPreviousResult to make sure ordering of results are
+ // consistent. See bug 412730 for more details.
+
+ // We want to store the original string with no leading or trailing
+ // whitespace for case sensitive searches.
+ this._originalSearchString = aSearchString.trim();
+
+ this._currentSearchString =
+ fixupSearchText(this._originalSearchString.toLowerCase());
+
+ let params = new Set(aSearchParam.split(" "));
+ this._enableActions = params.has("enable-actions");
+ this._disablePrivateActions = params.has("disable-private-actions");
+
+ this._listener = aListener;
+ let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
+ createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.setSearchString(aSearchString);
+ result.setListener(this);
+ this._result = result;
+
+ // If we are not enabled, we need to return now.
+ if (!this._enabled) {
+ this._finishSearch(true);
+ return;
+ }
+
+ // Reset our search behavior to the default.
+ if (this._currentSearchString) {
+ this._behavior = this._defaultBehavior;
+ }
+ else {
+ this._behavior = this._emptySearchDefaultBehavior;
+ }
+ // For any given search, we run up to four queries:
+ // 1) keywords (this._keywordQuery)
+ // 2) adaptive learning (this._adaptiveQuery)
+ // 3) open pages not supported by history (this._openPagesQuery)
+ // 4) query from this._getSearch
+ // (1) only gets ran if we get any filtered tokens from this._getSearch,
+ // since if there are no tokens, there is nothing to match, so there is no
+ // reason to run the query).
+ let {query, tokens} =
+ this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString));
+ let queries = tokens.length ?
+ [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery()] :
+ [this._getBoundAdaptiveQuery()];
+
+ if (this._hasBehavior("openpage")) {
+ queries.push(this._getBoundOpenPagesQuery(tokens));
+ }
+ queries.push(query);
+
+ // Start executing our queries.
+ this._telemetryStartTime = Date.now();
+ this._executeQueries(queries);
+
+ // Set up our persistent state for the duration of the search.
+ this._searchTokens = tokens;
+ this._usedPlaces = {};
+ },
+
+ stopSearch: function PAC_stopSearch()
+ {
+ // We need to cancel our searches so we do not get any [more] results.
+ // However, it's possible we haven't actually started any searches, so this
+ // method may throw because this._pendingQuery may be undefined.
+ if (this._pendingQuery) {
+ this._stopActiveQuery();
+ }
+
+ this._finishSearch(false);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAutoCompleteSimpleResultListener
+
+ onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB)
+ {
+ if (aRemoveFromDB) {
+ PlacesUtils.history.removePage(NetUtil.newURI(aURISpec));
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIPlacesAutoComplete
+
+ // If the connection has not yet been started, use this local cache. This
+ // prevents autocomplete from initing the database till the first search.
+ _openPagesCache: [],
+ registerOpenPage: function PAC_registerOpenPage(aURI)
+ {
+ if (!this._databaseInitialized) {
+ this._openPagesCache.push(aURI.spec);
+ return;
+ }
+
+ let stmt = this._registerOpenPageQuery;
+ stmt.params.page_url = aURI.spec;
+ stmt.executeAsync();
+ },
+
+ unregisterOpenPage: function PAC_unregisterOpenPage(aURI)
+ {
+ if (!this._databaseInitialized) {
+ let index = this._openPagesCache.indexOf(aURI.spec);
+ if (index != -1) {
+ this._openPagesCache.splice(index, 1);
+ }
+ return;
+ }
+
+ let stmt = this._unregisterOpenPageQuery;
+ stmt.params.page_url = aURI.spec;
+ stmt.executeAsync();
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIStorageStatementCallback
+
+ handleResult: function PAC_handleResult(aResultSet)
+ {
+ let row, haveMatches = false;
+ while ((row = aResultSet.getNextRow())) {
+ let match = this._processRow(row);
+ haveMatches = haveMatches || match;
+
+ if (this._result.matchCount == this._maxRichResults) {
+ // We have enough results, so stop running our search.
+ this._stopActiveQuery();
+
+ // And finish our search.
+ this._finishSearch(true);
+ return;
+ }
+
+ }
+
+ // Notify about results if we've gotten them.
+ if (haveMatches) {
+ this._notifyResults(true);
+ }
+ },
+
+ handleError: function PAC_handleError(aError)
+ {
+ Components.utils.reportError("Places AutoComplete: An async statement encountered an " +
+ "error: " + aError.result + ", '" + aError.message + "'");
+ },
+
+ handleCompletion: function PAC_handleCompletion(aReason)
+ {
+ // If we have already finished our search, we should bail out early.
+ if (this.isSearchComplete()) {
+ return;
+ }
+
+ // If we do not have enough results, and our match type is
+ // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
+ // results.
+ if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
+ this._result.matchCount < this._maxRichResults && !this._secondPass) {
+ this._secondPass = true;
+ let queries = [
+ this._getBoundAdaptiveQuery(MATCH_ANYWHERE),
+ this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens),
+ ];
+ this._executeQueries(queries);
+ return;
+ }
+
+ this._finishSearch(true);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+
+ observe: function PAC_observe(aSubject, aTopic, aData)
+ {
+ if (aTopic == kTopicShutdown) {
+ this._os.removeObserver(this, kTopicShutdown);
+
+ // Remove our preference observer.
+ this._prefs.removeObserver("", this);
+ delete this._prefs;
+
+ // Finalize the statements that we have used.
+ let stmts = [
+ "_defaultQuery",
+ "_historyQuery",
+ "_bookmarkQuery",
+ "_tagsQuery",
+ "_openPagesQuery",
+ "_typedQuery",
+ "_adaptiveQuery",
+ "_keywordQuery",
+ "_registerOpenPageQuery",
+ "_unregisterOpenPageQuery",
+ ];
+ for (let i = 0; i < stmts.length; i++) {
+ // We do not want to create any query we haven't already created, so
+ // see if it is a getter first.
+ if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) {
+ this[stmts[i]].finalize();
+ }
+ }
+
+ if (this._databaseInitialized) {
+ this._db.asyncClose();
+ }
+ }
+ else if (aTopic == kPrefChanged) {
+ // Avoid re-entrancy when flipping linked preferences.
+ if (this._ignoreNotifications)
+ return;
+ this._ignoreNotifications = true;
+ this._loadPrefs(false, aTopic, aData);
+ this._ignoreNotifications = false;
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsPlacesAutoComplete
+
+ get _databaseInitialized() {
+ return Object.getOwnPropertyDescriptor(this, "_db").value !== undefined;
+ },
+
+ /**
+ * Generates the tokens used in searching from a given string.
+ *
+ * @param aSearchString
+ * The string to generate tokens from.
+ * @return an array of tokens.
+ */
+ _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString)
+ {
+ // Calling split on an empty string will return an array containing one
+ // empty string. We don't want that, as it'll break our logic, so return an
+ // empty array then.
+ return aSearchString.length ? aSearchString.split(" ") : [];
+ },
+
+ /**
+ * Properly cleans up when searching is completed.
+ *
+ * @param aNotify
+ * Indicates if we should notify the AutoComplete listener about our
+ * results or not.
+ */
+ _finishSearch: function PAC_finishSearch(aNotify)
+ {
+ // Notify about results if we are supposed to.
+ if (aNotify) {
+ this._notifyResults(false);
+ }
+
+ // Clear our state
+ delete this._originalSearchString;
+ delete this._currentSearchString;
+ delete this._strippedPrefix;
+ delete this._searchTokens;
+ delete this._listener;
+ delete this._result;
+ delete this._usedPlaces;
+ delete this._pendingQuery;
+ this._secondPass = false;
+ this._enableActions = false;
+ },
+
+ /**
+ * Executes the given queries asynchronously.
+ *
+ * @param aQueries
+ * The queries to execute.
+ */
+ _executeQueries: function PAC_executeQueries(aQueries)
+ {
+ // Because we might get a handleCompletion for canceled queries, we want to
+ // filter out queries we no longer care about (described in the
+ // handleCompletion implementation of AutoCompleteStatementCallbackWrapper).
+
+ // Create our wrapper object and execute the queries.
+ let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db);
+ this._pendingQuery = wrapper.executeAsync(aQueries);
+ },
+
+ /**
+ * Stops executing our active query.
+ */
+ _stopActiveQuery: function PAC_stopActiveQuery()
+ {
+ this._pendingQuery.cancel();
+ delete this._pendingQuery;
+ },
+
+ /**
+ * Notifies the listener about results.
+ *
+ * @param aSearchOngoing
+ * Indicates if the search is ongoing or not.
+ */
+ _notifyResults: function PAC_notifyResults(aSearchOngoing)
+ {
+ let result = this._result;
+ let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+ if (aSearchOngoing) {
+ resultCode += "_ONGOING";
+ }
+ result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+ this._listener.onSearchResult(this, result);
+ if (this._telemetryStartTime) {
+ let elapsed = Date.now() - this._telemetryStartTime;
+ if (elapsed > 50) {
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS")
+ .add(elapsed);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ }
+ }
+ this._telemetryStartTime = null;
+ }
+ },
+
+ /**
+ * Synchronize suggest.* prefs with autocomplete.enabled.
+ */
+ _syncEnabledPref: function PAC_syncEnabledPref()
+ {
+ let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"];
+ let types = ["History", "Bookmark", "Openpage"];
+
+ this._enabled = safePrefGetter(this._prefs, kBrowserUrlbarAutocompleteEnabledPref,
+ true);
+ this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true);
+ this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true);
+ this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true);
+
+ if (this._enabled) {
+ // If the autocomplete preference is active, activate all suggest
+ // preferences only if all of them are false.
+ if (types.every(type => this["_suggest" + type] == false)) {
+ for (let type of suggestPrefs) {
+ this._prefs.setBoolPref(type, true);
+ }
+ }
+ } else {
+ // If the preference was deactivated, deactivate all suggest preferences.
+ for (let type of suggestPrefs) {
+ this._prefs.setBoolPref(type, false);
+ }
+ }
+ },
+
+ /**
+ * Loads the preferences that we care about.
+ *
+ * @param [optional] aRegisterObserver
+ * Indicates if the preference observer should be added or not. The
+ * default value is false.
+ * @param [optional] aTopic
+ * Observer's topic, if any.
+ * @param [optional] aSubject
+ * Observer's subject, if any.
+ */
+ _loadPrefs: function PAC_loadPrefs(aRegisterObserver, aTopic, aData)
+ {
+ // Avoid race conditions with UnifiedComplete component.
+ if (aData && !isUnifiedCompleteInstantiated()) {
+ // Synchronize suggest.* prefs with autocomplete.enabled.
+ if (aData == kBrowserUrlbarAutocompleteEnabledPref) {
+ this._syncEnabledPref();
+ } else if (aData.startsWith("suggest.")) {
+ let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"];
+ this._prefs.setBoolPref(kBrowserUrlbarAutocompleteEnabledPref,
+ suggestPrefs.some(pref => safePrefGetter(this._prefs, pref, true)));
+ }
+ }
+
+ this._enabled = safePrefGetter(this._prefs,
+ kBrowserUrlbarAutocompleteEnabledPref,
+ true);
+ this._matchBehavior = safePrefGetter(this._prefs,
+ "matchBehavior",
+ MATCH_BOUNDARY_ANYWHERE);
+ this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true);
+ this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25);
+ this._restrictHistoryToken = safePrefGetter(this._prefs,
+ "restrict.history", "^");
+ this._restrictBookmarkToken = safePrefGetter(this._prefs,
+ "restrict.bookmark", "*");
+ this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~");
+ this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+");
+ this._restrictOpenPageToken = safePrefGetter(this._prefs,
+ "restrict.openpage", "%");
+ this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#");
+ this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@");
+
+ this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true);
+ this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true);
+ this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true);
+ this._suggestTyped = safePrefGetter(this._prefs, "suggest.history.onlyTyped", false);
+
+ // If history is not set, onlyTyped value should be ignored.
+ if (!this._suggestHistory) {
+ this._suggestTyped = false;
+ }
+ let types = ["History", "Bookmark", "Openpage", "Typed"];
+ this._defaultBehavior = types.reduce((memo, type) => {
+ let prefValue = this["_suggest" + type];
+ return memo | (prefValue &&
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]);
+ }, 0);
+
+ // Further restrictions to apply for "empty searches" (i.e. searches for "").
+ // The empty behavior is typed history, if history is enabled. Otherwise,
+ // it is bookmarks, if they are enabled. If both history and bookmarks are disabled,
+ // it defaults to open pages.
+ this._emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
+ if (this._suggestHistory) {
+ this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+ Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
+ } else if (this._suggestBookmark) {
+ this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+ } else {
+ this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
+ }
+
+ // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+ if (this._matchBehavior != MATCH_ANYWHERE &&
+ this._matchBehavior != MATCH_BOUNDARY &&
+ this._matchBehavior != MATCH_BEGINNING) {
+ this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
+ }
+ // register observer
+ if (aRegisterObserver) {
+ this._prefs.addObserver("", this, false);
+ }
+ },
+
+ /**
+ * Given an array of tokens, this function determines which query should be
+ * ran. It also removes any special search tokens.
+ *
+ * @param aTokens
+ * An array of search tokens.
+ * @return an object with two properties:
+ * query: the correctly optimized, bound query to search the database
+ * with.
+ * tokens: the filtered list of tokens to search with.
+ */
+ _getSearch: function PAC_getSearch(aTokens)
+ {
+ let foundToken = false;
+ let restrict = (behavior) => {
+ if (!foundToken) {
+ this._behavior = 0;
+ this._setBehavior("restrict");
+ foundToken = true;
+ }
+ this._setBehavior(behavior);
+ };
+
+ // Set the proper behavior so our call to _getBoundSearchQuery gives us the
+ // correct query.
+ for (let i = aTokens.length - 1; i >= 0; i--) {
+ switch (aTokens[i]) {
+ case this._restrictHistoryToken:
+ restrict("history");
+ break;
+ case this._restrictBookmarkToken:
+ restrict("bookmark");
+ break;
+ case this._restrictTagToken:
+ restrict("tag");
+ break;
+ case this._restrictOpenPageToken:
+ if (!this._enableActions) {
+ continue;
+ }
+ restrict("openpage");
+ break;
+ case this._matchTitleToken:
+ restrict("title");
+ break;
+ case this._matchURLToken:
+ restrict("url");
+ break;
+ case this._restrictTypedToken:
+ restrict("typed");
+ break;
+ default:
+ // We do not want to remove the token if we did not match.
+ continue;
+ }
+
+ aTokens.splice(i, 1);
+ }
+
+ // Set the right JavaScript behavior based on our preference. Note that the
+ // preference is whether or not we should filter JavaScript, and the
+ // behavior is if we should search it or not.
+ if (!this._filterJavaScript) {
+ this._setBehavior("javascript");
+ }
+
+ return {
+ query: this._getBoundSearchQuery(this._matchBehavior, aTokens),
+ tokens: aTokens
+ };
+ },
+
+ /**
+ * @return a string consisting of the search query to be used based on the
+ * previously set urlbar suggestion preferences.
+ */
+ _getSuggestionPrefQuery: function PAC_getSuggestionPrefQuery()
+ {
+ if (!this._hasBehavior("restrict") && this._hasBehavior("history") &&
+ this._hasBehavior("bookmark")) {
+ return this._hasBehavior("typed") ? this._customQuery("AND h.typed = 1")
+ : this._defaultQuery;
+ }
+ let conditions = [];
+ if (this._hasBehavior("history")) {
+ // Enforce ignoring the visit_count index, since the frecency one is much
+ // faster in this case. ANALYZE helps the query planner to figure out the
+ // faster path, but it may not have up-to-date information yet.
+ conditions.push("+h.visit_count > 0");
+ }
+ if (this._hasBehavior("typed")) {
+ conditions.push("h.typed = 1");
+ }
+ if (this._hasBehavior("bookmark")) {
+ conditions.push("bookmarked");
+ }
+ if (this._hasBehavior("tag")) {
+ conditions.push("tags NOTNULL");
+ }
+
+ return conditions.length ? this._customQuery("AND " + conditions.join(" AND "))
+ : this._defaultQuery;
+ },
+
+ /**
+ * Obtains the search query to be used based on the previously set search
+ * behaviors (accessed by this._hasBehavior). The query is bound and ready to
+ * execute.
+ *
+ * @param aMatchBehavior
+ * How this query should match its tokens to the search string.
+ * @param aTokens
+ * An array of search tokens.
+ * @return the correctly optimized query to search the database with and the
+ * new list of tokens to search with. The query has all the needed
+ * parameters bound, so consumers can execute it without doing any
+ * additional work.
+ */
+ _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior,
+ aTokens)
+ {
+ let query = this._getSuggestionPrefQuery();
+
+ // Bind the needed parameters to the query so consumers can use it.
+ let params = query.params;
+ params.parent = PlacesUtils.tagsFolderId;
+ params.query_type = kQueryTypeFiltered;
+ params.matchBehavior = aMatchBehavior;
+ params.searchBehavior = this._behavior;
+
+ // We only want to search the tokens that we are left with - not the
+ // original search string.
+ params.searchString = aTokens.join(" ");
+
+ // Limit the query to the the maximum number of desired results.
+ // This way we can avoid doing more work than needed.
+ params.maxResults = this._maxRichResults;
+
+ return query;
+ },
+
+ _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens)
+ {
+ let query = this._openPagesQuery;
+
+ // Bind the needed parameters to the query so consumers can use it.
+ let params = query.params;
+ params.query_type = kQueryTypeFiltered;
+ params.matchBehavior = this._matchBehavior;
+ params.searchBehavior = this._behavior;
+
+ // We only want to search the tokens that we are left with - not the
+ // original search string.
+ params.searchString = aTokens.join(" ");
+ params.maxResults = this._maxRichResults;
+
+ return query;
+ },
+
+ /**
+ * Obtains the keyword query with the properly bound parameters.
+ *
+ * @param aTokens
+ * The array of search tokens to check against.
+ * @return the bound keyword query.
+ */
+ _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens)
+ {
+ // The keyword is the first word in the search string, with the parameters
+ // following it.
+ let searchString = this._originalSearchString;
+ let queryString = "";
+ let queryIndex = searchString.indexOf(" ");
+ if (queryIndex != -1) {
+ queryString = searchString.substring(queryIndex + 1);
+ }
+ // We need to escape the parameters as if they were the query in a URL
+ queryString = encodeURIComponent(queryString).replace(/%20/g, "+");
+
+ // The first word could be a keyword, so that's what we'll search.
+ let keyword = aTokens[0];
+
+ let query = this._keywordQuery;
+ let params = query.params;
+ params.keyword = keyword;
+ params.query_string = queryString;
+ params.query_type = kQueryTypeKeyword;
+
+ return query;
+ },
+
+ /**
+ * Obtains the adaptive query with the properly bound parameters.
+ *
+ * @return the bound adaptive query.
+ */
+ _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior)
+ {
+ // If we were not given a match behavior, use the stored match behavior.
+ if (arguments.length == 0) {
+ aMatchBehavior = this._matchBehavior;
+ }
+
+ let query = this._adaptiveQuery;
+ let params = query.params;
+ params.parent = PlacesUtils.tagsFolderId;
+ params.search_string = this._currentSearchString;
+ params.query_type = kQueryTypeFiltered;
+ params.matchBehavior = aMatchBehavior;
+ params.searchBehavior = this._behavior;
+
+ return query;
+ },
+
+ /**
+ * Processes a mozIStorageRow to generate the proper data for the AutoComplete
+ * result. This will add an entry to the current result if it matches the
+ * criteria.
+ *
+ * @param aRow
+ * The row to process.
+ * @return true if the row is accepted, and false if not.
+ */
+ _processRow: function PAC_processRow(aRow)
+ {
+ // Before we do any work, make sure this entry isn't already in our results.
+ let entryId = aRow.getResultByIndex(kQueryIndexPlaceId);
+ let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
+ let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0;
+
+ // If actions are enabled and the page is open, add only the switch-to-tab
+ // result. Otherwise, add the normal result.
+ let [url, action] = this._enableActions && openPageCount > 0 && this._hasBehavior("openpage") ?
+ ["moz-action:switchtab," + escapedEntryURL, "action "] :
+ [escapedEntryURL, ""];
+
+ if (this._inResults(entryId, url)) {
+ return false;
+ }
+
+ let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
+ let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || "";
+ let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked);
+ let entryBookmarkTitle = entryBookmarked ?
+ aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
+ let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
+
+ // Always prefer the bookmark title unless it is empty
+ let title = entryBookmarkTitle || entryTitle;
+
+ let style;
+ if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
+ style = "keyword";
+ title = NetUtil.newURI(escapedEntryURL).host;
+ }
+
+ // We will always prefer to show tags if we have them.
+ let showTags = !!entryTags;
+
+ // However, we'll act as if a page is not bookmarked if the user wants
+ // only history and not bookmarks and there are no tags.
+ if (this._hasBehavior("history") && !this._hasBehavior("bookmark") &&
+ !showTags) {
+ showTags = false;
+ style = "favicon";
+ }
+
+ // If we have tags and should show them, we need to add them to the title.
+ if (showTags) {
+ title += kTitleTagsSeparator + entryTags;
+ }
+ // We have to determine the right style to display. Tags show the tag icon,
+ // bookmarks get the bookmark icon, and keywords get the keyword icon. If
+ // the result does not fall into any of those, it just gets the favicon.
+ if (!style) {
+ // It is possible that we already have a style set (from a keyword
+ // search or because of the user's preferences), so only set it if we
+ // haven't already done so.
+ if (showTags) {
+ style = "tag";
+ }
+ else if (entryBookmarked) {
+ style = "bookmark";
+ }
+ else {
+ style = "favicon";
+ }
+ }
+
+ this._addToResults(entryId, url, title, entryFavicon, action + style);
+ return true;
+ },
+
+ /**
+ * Checks to see if the given place has already been added to the results.
+ *
+ * @param aPlaceId
+ * The place id to check for, may be null.
+ * @param aUrl
+ * The url to check for.
+ * @return true if the place has been added, false otherwise.
+ *
+ * @note Must check both the id and the url for a negative match, since
+ * autocomplete may run in the middle of a new page addition. In such
+ * a case the switch-to-tab query would hash the page by url, then a
+ * next query, running after the page addition, would hash it by id.
+ * It's not possible to just rely on url though, since keywords
+ * dynamically modify the url to include their search string.
+ */
+ _inResults: function PAC_inResults(aPlaceId, aUrl)
+ {
+ if (aPlaceId && aPlaceId in this._usedPlaces) {
+ return true;
+ }
+ return aUrl in this._usedPlaces;
+ },
+
+ /**
+ * Adds a result to the AutoComplete results. Also tracks that we've added
+ * this place_id into the result set.
+ *
+ * @param aPlaceId
+ * The place_id of the item to be added to the result set. This is
+ * used by _inResults.
+ * @param aURISpec
+ * The URI spec for the entry.
+ * @param aTitle
+ * The title to give the entry.
+ * @param aFaviconSpec
+ * The favicon to give to the entry.
+ * @param aStyle
+ * Indicates how the entry should be styled when displayed.
+ */
+ _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle,
+ aFaviconSpec, aStyle)
+ {
+ // Add this to our internal tracker to ensure duplicates do not end up in
+ // the result. _usedPlaces is an Object that is being used as a set.
+ // Not all entries have a place id, thus we fallback to the url for them.
+ // We cannot use only the url since keywords entries are modified to
+ // include the search string, and would be returned multiple times. Ids
+ // are faster too.
+ this._usedPlaces[aPlaceId || aURISpec] = true;
+
+ // Obtain the favicon for this URI.
+ let favicon;
+ if (aFaviconSpec) {
+ let uri = NetUtil.newURI(aFaviconSpec);
+ favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec;
+ }
+ favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec;
+
+ this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
+ },
+
+ /**
+ * Determines if the specified AutoComplete behavior is set.
+ *
+ * @param aType
+ * The behavior type to test for.
+ * @return true if the behavior is set, false otherwise.
+ */
+ _hasBehavior: function PAC_hasBehavior(aType)
+ {
+ let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
+
+ if (this._disablePrivateActions &&
+ behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE) {
+ return false;
+ }
+
+ return this._behavior & behavior;
+ },
+
+ /**
+ * Enables the desired AutoComplete behavior.
+ *
+ * @param aType
+ * The behavior type to set.
+ */
+ _setBehavior: function PAC_setBehavior(aType)
+ {
+ this._behavior |=
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
+ },
+
+ /**
+ * Determines if we are done searching or not.
+ *
+ * @return true if we have completed searching, false otherwise.
+ */
+ isSearchComplete: function PAC_isSearchComplete()
+ {
+ // If _pendingQuery is null, we should no longer do any work since we have
+ // already called _finishSearch. This means we completed our search.
+ return this._pendingQuery == null;
+ },
+
+ /**
+ * Determines if the given handle of a pending statement is a pending search
+ * or not.
+ *
+ * @param aHandle
+ * A mozIStoragePendingStatement to check and see if we are waiting for
+ * results from it still.
+ * @return true if it is a pending query, false otherwise.
+ */
+ isPendingSearch: function PAC_isPendingSearch(aHandle)
+ {
+ return this._pendingQuery == aHandle;
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteSimpleResultListener,
+ Ci.mozIPlacesAutoComplete,
+ Ci.mozIStorageStatementCallback,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// urlInlineComplete class
+//// component @mozilla.org/autocomplete/search;1?name=urlinline
+
+function urlInlineComplete()
+{
+ this._loadPrefs(true);
+ Services.obs.addObserver(this, kTopicShutdown, true);
+}
+
+urlInlineComplete.prototype = {
+
+/////////////////////////////////////////////////////////////////////////////////
+//// Database and query getters
+
+ __db: null,
+
+ get _db()
+ {
+ if (!this.__db && this._autofillEnabled) {
+ this.__db = PlacesUtils.history.DBConnection.clone(true);
+ }
+ return this.__db;
+ },
+
+ __hostQuery: null,
+
+ get _hostQuery()
+ {
+ if (!this.__hostQuery) {
+ // Add a trailing slash at the end of the hostname, since we always
+ // want to complete up to and including a URL separator.
+ this.__hostQuery = this._db.createAsyncStatement(
+ `/* do not warn (bug no): could index on (typed,frecency) but not worth it */
+ SELECT host || '/', prefix || host || '/'
+ FROM moz_hosts
+ WHERE host BETWEEN :search_string AND :search_string || X'FFFF'
+ AND frecency <> 0
+ ${this._autofillTyped ? "AND typed = 1" : ""}
+ ORDER BY frecency DESC
+ LIMIT 1`
+ );
+ }
+ return this.__hostQuery;
+ },
+
+ __urlQuery: null,
+
+ get _urlQuery()
+ {
+ if (!this.__urlQuery) {
+ this.__urlQuery = this._db.createAsyncStatement(
+ `/* do not warn (bug no): can't use an index */
+ SELECT h.url
+ FROM moz_places h
+ WHERE h.frecency <> 0
+ ${this._autofillTyped ? "AND h.typed = 1 " : ""}
+ AND AUTOCOMPLETE_MATCH(:searchString, h.url,
+ h.title, '',
+ h.visit_count, h.typed, 0, 0,
+ :matchBehavior, :searchBehavior)
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT 1`
+ );
+ }
+ return this.__urlQuery;
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAutoCompleteSearch
+
+ startSearch: function UIC_startSearch(aSearchString, aSearchParam,
+ aPreviousResult, aListener)
+ {
+ // Stop the search in case the controller has not taken care of it.
+ if (this._pendingQuery) {
+ this.stopSearch();
+ }
+
+ let pendingSearch = this._pendingSearch = {};
+
+ // We want to store the original string with no leading or trailing
+ // whitespace for case sensitive searches.
+ this._originalSearchString = aSearchString;
+ this._currentSearchString =
+ fixupSearchText(this._originalSearchString.toLowerCase());
+ // The protocol and the host are lowercased by nsIURI, so it's fine to
+ // lowercase the typed prefix to add it back to the results later.
+ this._strippedPrefix = this._originalSearchString.slice(
+ 0, this._originalSearchString.length - this._currentSearchString.length
+ ).toLowerCase();
+
+ this._result = Cc["@mozilla.org/autocomplete/simple-result;1"].
+ createInstance(Ci.nsIAutoCompleteSimpleResult);
+ this._result.setSearchString(aSearchString);
+ this._result.setTypeAheadResult(true);
+
+ this._listener = aListener;
+
+ Task.spawn(function* () {
+ // Don't autoFill if the search term is recognized as a keyword, otherwise
+ // it will override default keywords behavior. Note that keywords are
+ // hashed on first use, so while the first query may delay a little bit,
+ // next ones will just hit the memory hash.
+ let dontAutoFill = this._currentSearchString.length == 0 || !this._db ||
+ (yield PlacesUtils.keywords.fetch(this._currentSearchString));
+ if (this._pendingSearch != pendingSearch)
+ return;
+ if (dontAutoFill) {
+ this._finishSearch();
+ return;
+ }
+
+ // Don't try to autofill if the search term includes any whitespace.
+ // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+ // tokenizer ends up trimming the search string and returning a value
+ // that doesn't match it, or is even shorter.
+ if (/\s/.test(this._currentSearchString)) {
+ this._finishSearch();
+ return;
+ }
+
+ // Hosts have no "/" in them.
+ let lastSlashIndex = this._currentSearchString.lastIndexOf("/");
+
+ // Search only URLs if there's a slash in the search string...
+ if (lastSlashIndex != -1) {
+ // ...but not if it's exactly at the end of the search string.
+ if (lastSlashIndex < this._currentSearchString.length - 1)
+ this._queryURL();
+ else
+ this._finishSearch();
+ return;
+ }
+
+ // Do a synchronous search on the table of hosts.
+ let query = this._hostQuery;
+ query.params.search_string = this._currentSearchString.toLowerCase();
+ // This is just to measure the delay to reach the UI, not the query time.
+ TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
+ let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
+ handleResult: aResultSet => {
+ if (this._pendingSearch != pendingSearch)
+ return;
+ let row = aResultSet.getNextRow();
+ let trimmedHost = row.getResultByIndex(0);
+ let untrimmedHost = row.getResultByIndex(1);
+ // If the untrimmed value doesn't preserve the user's input just
+ // ignore it and complete to the found host.
+ if (untrimmedHost &&
+ !untrimmedHost.toLowerCase().includes(this._originalSearchString.toLowerCase())) {
+ untrimmedHost = null;
+ }
+
+ this._result.appendMatch(this._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);
+
+ // handleCompletion() will cause the result listener to be called, and
+ // will display the result in the UI.
+ },
+
+ handleError: aError => {
+ Components.utils.reportError(
+ "URL Inline Complete: An async statement encountered an " +
+ "error: " + aError.result + ", '" + aError.message + "'");
+ },
+
+ handleCompletion: aReason => {
+ if (this._pendingSearch != pendingSearch)
+ return;
+ TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
+ this._finishSearch();
+ }
+ }, this._db);
+ this._pendingQuery = wrapper.executeAsync([query]);
+ }.bind(this));
+ },
+
+ /**
+ * Execute an asynchronous search through places, and complete
+ * up to the next URL separator.
+ */
+ _queryURL: function UIC__queryURL()
+ {
+ // The URIs in the database are fixed up, so we can match on a lowercased
+ // host, but the path must be matched in a case sensitive way.
+ let pathIndex =
+ this._originalSearchString.indexOf("/", this._strippedPrefix.length);
+ this._currentSearchString = fixupSearchText(
+ this._originalSearchString.slice(0, pathIndex).toLowerCase() +
+ this._originalSearchString.slice(pathIndex)
+ );
+
+ // Within the standard autocomplete query, we only search the beginning
+ // of URLs for 1 result.
+ let query = this._urlQuery;
+ let params = query.params;
+ params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE;
+ params.searchBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+ Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED |
+ Ci.mozIPlacesAutoComplete.BEHAVIOR_URL;
+ params.searchString = this._currentSearchString;
+
+ // Execute the query.
+ let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
+ handleResult: aResultSet => {
+ let row = aResultSet.getNextRow();
+ let value = row.getResultByIndex(0);
+ let url = fixupSearchText(value);
+
+ let prefix = value.slice(0, value.length - stripPrefix(value).length);
+
+ // We must complete the URL up to the next separator (which is /, ? or #).
+ let separatorIndex = url.slice(this._currentSearchString.length)
+ .search(/[\/\?\#]/);
+ if (separatorIndex != -1) {
+ separatorIndex += this._currentSearchString.length;
+ if (url[separatorIndex] == "/") {
+ separatorIndex++; // Include the "/" separator
+ }
+ url = url.slice(0, separatorIndex);
+ }
+
+ // Add the result.
+ // If the untrimmed value doesn't preserve the user's input just
+ // ignore it and complete to the found url.
+ let untrimmedURL = prefix + url;
+ if (untrimmedURL &&
+ !untrimmedURL.toLowerCase().includes(this._originalSearchString.toLowerCase())) {
+ untrimmedURL = null;
+ }
+
+ this._result.appendMatch(this._strippedPrefix + url, "", "", "", untrimmedURL);
+
+ // handleCompletion() will cause the result listener to be called, and
+ // will display the result in the UI.
+ },
+
+ handleError: aError => {
+ Components.utils.reportError(
+ "URL Inline Complete: An async statement encountered an " +
+ "error: " + aError.result + ", '" + aError.message + "'");
+ },
+
+ handleCompletion: aReason => {
+ this._finishSearch();
+ }
+ }, this._db);
+ this._pendingQuery = wrapper.executeAsync([query]);
+ },
+
+ stopSearch: function UIC_stopSearch()
+ {
+ delete this._originalSearchString;
+ delete this._currentSearchString;
+ delete this._result;
+ delete this._listener;
+ delete this._pendingSearch;
+
+ if (this._pendingQuery) {
+ this._pendingQuery.cancel();
+ delete this._pendingQuery;
+ }
+ },
+
+ /**
+ * Loads the preferences that we care about.
+ *
+ * @param [optional] aRegisterObserver
+ * Indicates if the preference observer should be added or not. The
+ * default value is false.
+ */
+ _loadPrefs: function UIC_loadPrefs(aRegisterObserver)
+ {
+ let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch);
+ let autocomplete = safePrefGetter(prefBranch,
+ kBrowserUrlbarAutocompleteEnabledPref,
+ true);
+ let autofill = safePrefGetter(prefBranch,
+ kBrowserUrlbarAutofillPref,
+ true);
+ this._autofillEnabled = autocomplete && autofill;
+ this._autofillTyped = safePrefGetter(prefBranch,
+ kBrowserUrlbarAutofillTypedPref,
+ true);
+ if (aRegisterObserver) {
+ Services.prefs.addObserver(kBrowserUrlbarBranch, this, true);
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAutoCompleteSearchDescriptor
+
+ get searchType() {
+ return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
+ },
+
+ get clearingAutoFillSearchesAgain() {
+ return false;
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+
+ observe: function UIC_observe(aSubject, aTopic, aData)
+ {
+ if (aTopic == kTopicShutdown) {
+ this._closeDatabase();
+ }
+ else if (aTopic == kPrefChanged &&
+ (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref ||
+ aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref ||
+ aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) {
+ let previousAutofillTyped = this._autofillTyped;
+ this._loadPrefs();
+ if (!this._autofillEnabled) {
+ this.stopSearch();
+ this._closeDatabase();
+ }
+ else if (this._autofillTyped != previousAutofillTyped) {
+ // Invalidate the statements to update them for the new typed status.
+ this._invalidateStatements();
+ }
+ }
+ },
+
+ /**
+ * Finalizes and invalidates cached statements.
+ */
+ _invalidateStatements: function UIC_invalidateStatements()
+ {
+ // Finalize the statements that we have used.
+ let stmts = [
+ "__hostQuery",
+ "__urlQuery",
+ ];
+ for (let i = 0; i < stmts.length; i++) {
+ // We do not want to create any query we haven't already created, so
+ // see if it is a getter first.
+ if (this[stmts[i]]) {
+ this[stmts[i]].finalize();
+ this[stmts[i]] = null;
+ }
+ }
+ },
+
+ /**
+ * Closes the database.
+ */
+ _closeDatabase: function UIC_closeDatabase()
+ {
+ this._invalidateStatements();
+ if (this.__db) {
+ this._db.asyncClose();
+ this.__db = null;
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// urlInlineComplete
+
+ _finishSearch: function UIC_finishSearch()
+ {
+ // Notify the result object
+ let result = this._result;
+
+ if (result.matchCount) {
+ result.setDefaultIndex(0);
+ result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]);
+ } else {
+ result.setDefaultIndex(-1);
+ result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]);
+ }
+
+ this._listener.onSearchResult(this, result);
+ this.stopSearch();
+ },
+
+ isSearchComplete: function UIC_isSearchComplete()
+ {
+ return this._pendingQuery == null;
+ },
+
+ isPendingSearch: function UIC_isPendingSearch(aHandle)
+ {
+ return this._pendingQuery == aHandle;
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteSearchDescriptor,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ])
+};
+
+var components = [nsPlacesAutoComplete, urlInlineComplete];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/components/places/nsPlacesAutoComplete.manifest b/toolkit/components/places/nsPlacesAutoComplete.manifest
new file mode 100644
index 000000000..eb704f449
--- /dev/null
+++ b/toolkit/components/places/nsPlacesAutoComplete.manifest
@@ -0,0 +1,6 @@
+component {d0272978-beab-4adc-a3d4-04b76acfa4e7} nsPlacesAutoComplete.js
+contract @mozilla.org/autocomplete/search;1?name=history {d0272978-beab-4adc-a3d4-04b76acfa4e7}
+
+component {c88fae2d-25cf-4338-a1f4-64a320ea7440} nsPlacesAutoComplete.js
+contract @mozilla.org/autocomplete/search;1?name=urlinline {c88fae2d-25cf-4338-a1f4-64a320ea7440}
+
diff --git a/toolkit/components/places/nsTaggingService.js b/toolkit/components/places/nsTaggingService.js
index 1fad67a82..e367e6cb3 100644
--- a/toolkit/components/places/nsTaggingService.js
+++ b/toolkit/components/places/nsTaggingService.js
@@ -528,6 +528,10 @@ TagAutoCompleteResult.prototype = {
return this._results.length;
},
+ get typeAheadResult() {
+ return false;
+ },
+
/**
* Get the value of the result at the given index
*/
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/customizeToolbar.js b/toolkit/content/customizeToolbar.js
index b96b60b98..7400aaadc 100644
--- a/toolkit/content/customizeToolbar.js
+++ b/toolkit/content/customizeToolbar.js
@@ -2,6 +2,8 @@
* 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/. */
+const gToolbarInfoSeparators = ["|", "-"];
+
var gToolboxDocument = null;
var gToolbox = null;
var gCurrentDragOverItem = null;
@@ -173,9 +175,20 @@ function persistCurrentSets()
// Remove custom toolbars whose contents have been removed.
gToolbox.removeChild(toolbar);
} else if (gToolbox.toolbarset) {
+ var hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
+ "autohide" : "collapsed";
// Persist custom toolbar info on the <toolbarset/>
- gToolbox.toolbarset.setAttribute("toolbar"+(++customCount),
- toolbar.toolbarName + ":" + currentSet);
+ // Attributes:
+ // Names: "toolbarX" (X - the number of the toolbar)
+ // Values: "Name|HidingAttributeName-HidingAttributeValue|CurrentSet"
+ gToolbox.toolbarset.setAttribute("toolbar" + (++customCount),
+ toolbar.toolbarName
+ + gToolbarInfoSeparators[0]
+ + hidingAttribute
+ + gToolbarInfoSeparators[1]
+ + toolbar.getAttribute(hidingAttribute)
+ + gToolbarInfoSeparators[0]
+ + currentSet);
gToolboxDocument.persist(gToolbox.toolbarset.id, "toolbar"+customCount);
}
}
@@ -485,6 +498,11 @@ function addNewToolbar()
continue;
}
+ if (name.value.includes(gToolbarInfoSeparators[0])) {
+ message = stringBundle.getFormattedString("enterToolbarIllegalChars", [name.value]);
+ continue;
+ }
+
var dupeFound = false;
// Check for an existing toolbar with the same display name
@@ -506,7 +524,7 @@ function addNewToolbar()
message = stringBundle.getFormattedString("enterToolbarDup", [name.value]);
}
- gToolbox.appendCustomToolbar(name.value, "");
+ gToolbox.appendCustomToolbar(name.value, "", [null, null]);
toolboxChanged();
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/content/widgets/toolbar.xml b/toolkit/content/widgets/toolbar.xml
index 548504e24..e1f58f7aa 100644
--- a/toolkit/content/widgets/toolbar.xml
+++ b/toolkit/content/widgets/toolbar.xml
@@ -30,7 +30,7 @@
</field>
<field name="externalToolbars">
- []
+ []
</field>
<!-- Set by customizeToolbar.js -->
@@ -49,18 +49,54 @@
<constructor>
<![CDATA[
+ this.toolbarInfoSeparators = ["|", "-"];
+ this.toolbarInfoLegacySeparator = ":";
// Look to see if there is a toolbarset.
this.toolbarset = this.firstChild;
- while (this.toolbarset && this.toolbarset.localName != "toolbarset")
+ while (this.toolbarset && this.toolbarset.localName != "toolbarset") {
this.toolbarset = toolbarset.nextSibling;
+ }
if (this.toolbarset) {
// Create each toolbar described by the toolbarset.
var index = 0;
- while (toolbarset.hasAttribute("toolbar"+(++index))) {
- var toolbarInfo = toolbarset.getAttribute("toolbar"+index);
- var infoSplit = toolbarInfo.split(":");
- this.appendCustomToolbar(infoSplit[0], infoSplit[1]);
+ while (this.toolbarset.hasAttribute("toolbar" + (++index))) {
+ let hiddingAttribute =
+ this.toolbarset.getAttribute("type") == "menubar"
+ ? "autohide" : "collapsed";
+ let toolbarInfo = this.toolbarset.getAttribute("toolbar" + index);
+ let infoSplit = toolbarInfo.split(this.toolbarInfoSeparators[0]);
+ if (infoSplit.length == 1) {
+ infoSplit = toolbarInfo.split(this.toolbarInfoLegacySeparator);
+ }
+ let infoName = infoSplit[0];
+ let infoHidingAttribute = [null, null];
+ let infoCurrentSet = "";
+ let infoSplitLen = infoSplit.length;
+ switch (infoSplitLen) {
+ case 3:
+ // Pale Moon 27.2+
+ // Basilisk (UXP)
+ infoHidingAttribute = infoSplit[1]
+ .split(this.toolbarInfoSeparators[1]);
+ infoCurrentSet = infoSplit[2];
+ break;
+ case 2:
+ // Legacy:
+ // - toolbars from Pale Moon 27.0 - 27.1.x
+ // - Basilisk (moebius)
+ // The previous value (hiddingAttribute) isn't stored.
+ infoHidingAttribute = [hiddingAttribute, "false"];
+ infoCurrentSet = infoSplit[1];
+ break;
+ default:
+ Components.utils.reportError(
+ "Customizable toolbars - an invalid value:" + "\n"
+ + '"toolbar' + index + '" = "' + toolbarInfo + '"');
+ break;
+ }
+ this.appendCustomToolbar(
+ infoName, infoCurrentSet, infoHidingAttribute);
}
}
]]>
@@ -69,6 +105,7 @@
<method name="appendCustomToolbar">
<parameter name="aName"/>
<parameter name="aCurrentSet"/>
+ <parameter name="aHidingAttribute"/>
<body>
<![CDATA[
if (!this.toolbarset)
@@ -84,6 +121,10 @@
toolbar.setAttribute("iconsize", this.getAttribute("iconsize"));
toolbar.setAttribute("context", this.toolbarset.getAttribute("context"));
toolbar.setAttribute("class", "chromeclass-toolbar");
+ // Restore persist the hiding attribute.
+ if (aHidingAttribute[0]) {
+ toolbar.setAttribute(aHidingAttribute[0], aHidingAttribute[1]);
+ }
this.insertBefore(toolbar, this.toolbarset);
return toolbar;
diff --git a/toolkit/locales/en-US/chrome/global/autocomplete.properties b/toolkit/locales/en-US/chrome/global/autocomplete.properties
index 5c7c84b96..44da643ac 100644
--- a/toolkit/locales/en-US/chrome/global/autocomplete.properties
+++ b/toolkit/locales/en-US/chrome/global/autocomplete.properties
@@ -1,6 +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/.
+switchToTab = Switch to tab
+
+# LOCALIZATION NOTE (visitURL):
+# %S is the URL to visit.
+visitURL = Visit %S
# LOCALIZATION NOTE (searchWithEngine): %S will be replaced with
# the search engine provider's name. This format was chosen because
diff --git a/toolkit/locales/en-US/chrome/global/customizeToolbar.properties b/toolkit/locales/en-US/chrome/global/customizeToolbar.properties
index 0ec6d2c1d..b19152fab 100644
--- a/toolkit/locales/en-US/chrome/global/customizeToolbar.properties
+++ b/toolkit/locales/en-US/chrome/global/customizeToolbar.properties
@@ -5,6 +5,7 @@
enterToolbarTitle=New Toolbar
enterToolbarName=Enter a name for this toolbar:
enterToolbarDup=There is already a toolbar with the name “%S”. Please enter a different name.
+enterToolbarIllegalChars=The name contains illegal character "|". Please enter a different name.
enterToolbarBlank=You must enter a name to create a new toolbar.
separatorTitle=Separator
springTitle=Flexible Space
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/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm
index 681c4240a..3913c2088 100644
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -88,9 +88,9 @@ Cu.import("resource://gre/modules/Log.jsm");
// Configure a logger at the parent 'addons' level to format
// messages for all the modules under addons.*
const PARENT_LOGGER_ID = "addons";
-let parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
+var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
parentLogger.level = Log.Level.Warn;
-let formatter = new Log.BasicFormatter();
+var formatter = new Log.BasicFormatter();
// Set parent logger (and its children) to append to
// the Javascript section of the Browser Console
parentLogger.addAppender(new Log.ConsoleAppender(formatter));
@@ -101,7 +101,7 @@ parentLogger.addAppender(new Log.DumpAppender(formatter));
// Create a new logger (child of 'addons' logger)
// for use by the Addons Manager
const LOGGER_ID = "addons.manager";
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
// Provide the ability to enable/disable logging
// messages at runtime.
diff --git a/toolkit/mozapps/extensions/DeferredSave.jsm b/toolkit/mozapps/extensions/DeferredSave.jsm
index d7f5b8864..7587ce83b 100644
--- a/toolkit/mozapps/extensions/DeferredSave.jsm
+++ b/toolkit/mozapps/extensions/DeferredSave.jsm
@@ -12,7 +12,7 @@ Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
// Make it possible to mock out timers for testing
-let MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+var MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.EXPORTED_SYMBOLS = ["DeferredSave"];
@@ -23,9 +23,9 @@ Cu.import("resource://gre/modules/Log.jsm");
//Configure a logger at the parent 'DeferredSave' level to format
//messages for all the modules under DeferredSave.*
const DEFERREDSAVE_PARENT_LOGGER_ID = "DeferredSave";
-let parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID);
+var parentLogger = Log.repository.getLogger(DEFERREDSAVE_PARENT_LOGGER_ID);
parentLogger.level = Log.Level.Warn;
-let formatter = new Log.BasicFormatter();
+var formatter = new Log.BasicFormatter();
//Set parent logger (and its children) to append to
//the Javascript section of the Browser Console
parentLogger.addAppender(new Log.ConsoleAppender(formatter));
diff --git a/toolkit/mozapps/extensions/addonManager.js b/toolkit/mozapps/extensions/addonManager.js
index 862b1ea69..731e70c6c 100644
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -31,9 +31,9 @@ const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
-let gSingleton = null;
+var gSingleton = null;
-let gParentMM = null;
+var gParentMM = null;
function amManager() {
diff --git a/toolkit/mozapps/extensions/amInstallTrigger.js b/toolkit/mozapps/extensions/amInstallTrigger.js
index b83cbe60b..a18fe84c4 100644
--- a/toolkit/mozapps/extensions/amInstallTrigger.js
+++ b/toolkit/mozapps/extensions/amInstallTrigger.js
@@ -18,7 +18,7 @@ const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage";
const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
-let log = Log.repository.getLogger("AddonManager.InstallTrigger");
+var log = Log.repository.getLogger("AddonManager.InstallTrigger");
log.level = Log.Level[Preferences.get("extensions.logging.enabled", false) ? "Warn" : "Trace"];
function CallbackObject(id, callback, urls, mediator) {
diff --git a/toolkit/mozapps/extensions/amWebInstallListener.js b/toolkit/mozapps/extensions/amWebInstallListener.js
index 901beef07..ac6e2495d 100644
--- a/toolkit/mozapps/extensions/amWebInstallListener.js
+++ b/toolkit/mozapps/extensions/amWebInstallListener.js
@@ -37,7 +37,7 @@ const LOGGER_ID = "addons.weblistener";
// Create a new logger for use by the Addons Web Listener
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
function notifyObservers(aTopic, aBrowser, aUri, aInstalls) {
let info = {
diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js
index 6f2a47482..8d9c132e6 100644
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -2456,7 +2456,7 @@ var gSearchView = {
this._allResultsLink.setAttribute("href",
AddonRepository.getSearchURL(this._lastQuery));
this._allResultsLink.hidden = false;
- },
+ },
updateListAttributes: function gSearchView_updateListAttributes() {
var item = this._listBox.querySelector("richlistitem[remote='true'][first]");
diff --git a/toolkit/mozapps/extensions/content/update.js b/toolkit/mozapps/extensions/content/update.js
index 3d87f4d4b..afc74dca8 100644
--- a/toolkit/mozapps/extensions/content/update.js
+++ b/toolkit/mozapps/extensions/content/update.js
@@ -22,7 +22,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modul
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
-let logger = null;
+var logger = null;
var gUpdateWizard = {
// When synchronizing app compatibility info this contains all installed
@@ -169,7 +169,7 @@ var gOfflinePage = {
}
// Addon listener to count addons enabled/disabled by metadata checks
-let listener = {
+var listener = {
onDisabled: function listener_onDisabled(aAddon) {
gUpdateWizard.affectedAddonIDs.add(aAddon.id);
gUpdateWizard.metadataDisabled++;
diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
index adcecbee7..76a7528c7 100644
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -72,7 +72,7 @@ const LOGGER_ID = "addons.repository";
// Create a new logger for use by the Addons Repository
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
// A map between XML keys to AddonSearchResult keys for string values
// that require no extra parsing from XML
@@ -101,7 +101,7 @@ const INTEGER_KEY_MAP = {
};
// Wrap the XHR factory so that tests can override with a mock
-let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
+var XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
"nsIXMLHttpRequest");
function convertHTMLToPlainText(html) {
diff --git a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
index 128146bbe..11944ddf5 100644
--- a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
@@ -30,7 +30,7 @@ const LOGGER_ID = "addons.repository.sqlmigrator";
// Create a new logger for use by the Addons Repository SQL Migrator
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"];
diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
index d68a0f175..939e2e269 100644
--- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
@@ -52,7 +52,7 @@ const LOGGER_ID = "addons.update-checker";
// Create a new logger for use by the Addons Update Checker
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
/**
* A serialisation method for RDF data that produces an identical string
diff --git a/toolkit/mozapps/extensions/internal/Content.js b/toolkit/mozapps/extensions/internal/Content.js
index 29c0ed8ce..61a8b0323 100644
--- a/toolkit/mozapps/extensions/internal/Content.js
+++ b/toolkit/mozapps/extensions/internal/Content.js
@@ -8,9 +8,9 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+var {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+var nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
"initWithPath");
const MSG_JAR_FLUSH = "AddonJarFlush";
diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
index a55457f6e..52affa9ba 100644
--- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
@@ -64,11 +64,11 @@ XPCOMUtils.defineLazyGetter(this, "pluginsBundle",
XPCOMUtils.defineLazyGetter(this, "gmpService",
() => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService));
-let messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+var messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
-let gLogger;
-let gLogAppenderDump = null;
+var gLogger;
+var gLogAppenderDump = null;
function configureLogging() {
if (!gLogger) {
@@ -443,7 +443,7 @@ GMPWrapper.prototype = {
},
};
-let GMPProvider = {
+var GMPProvider = {
get name() { return "GMPProvider"; },
_plugins: null,
diff --git a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
index fccde9a81..1e7d6b0d8 100644
--- a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
+++ b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
@@ -49,7 +49,7 @@ this.LightweightThemeImageOptimizer = {
Object.freeze(LightweightThemeImageOptimizer);
-let ImageCropper = {
+var ImageCropper = {
_inProgress: {},
getCroppedImageURL:
@@ -119,7 +119,7 @@ let ImageCropper = {
}
};
-let ImageFile = {
+var ImageFile = {
read: function ImageFile_read(aURI, aCallback) {
this._netUtil.asyncFetch2(
aURI,
@@ -158,7 +158,7 @@ let ImageFile = {
XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil",
"resource://gre/modules/NetUtil.jsm", "NetUtil");
-let ImageTools = {
+var ImageTools = {
decode: function ImageTools_decode(aInputStream, aContentType) {
let outParam = {value: null};
@@ -187,7 +187,7 @@ let ImageTools = {
XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools",
"@mozilla.org/image/tools;1", "imgITools");
-let Utils = {
+var Utils = {
createCopy: function Utils_createCopy(aData) {
let copy = {};
for (let [k, v] in Iterator(aData)) {
diff --git a/toolkit/mozapps/extensions/internal/PluginProvider.jsm b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
index 04a4f9d7c..cb07dcb12 100644
--- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -23,7 +23,7 @@ const LOGGER_ID = "addons.plugins";
// Create a new logger for use by the Addons Plugin Provider
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
function getIDHashForString(aStr) {
// return the two-digit hexadecimal code for a byte
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index d5f1ab5dd..2c5e3dfa7 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -129,7 +129,7 @@ const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
const TOOLKIT_ID = "toolkit@mozilla.org";
#ifdef MOZ_PHOENIX_EXTENSIONS
const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
-const FIREFOX_APPCOMPATVERSION = "27.9"
+const FIREFOX_APPCOMPATVERSION = "56.9"
#endif
// The value for this is in Makefile.in
@@ -222,7 +222,7 @@ const LOGGER_ID = "addons.xpi";
// Create a new logger for use by all objects in this Addons XPI Provider module
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
const LAZY_OBJECTS = ["XPIDatabase"];
@@ -7822,7 +7822,7 @@ WinRegInstallLocation.prototype = {
};
#endif
-let addonTypes = [
+var addonTypes = [
new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
STRING_TYPE_NAME,
AddonManager.VIEW_TYPE_LIST, 4000,
diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
index 2cef907f1..d26029455 100644
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -29,7 +29,7 @@ const LOGGER_ID = "addons.xpi-utils";
// Create a new logger for use by the Addons XPI Provider Utils
// (Requires AddonManager.jsm)
-let logger = Log.repository.getLogger(LOGGER_ID);
+var logger = Log.repository.getLogger(LOGGER_ID);
const KEY_PROFILEDIR = "ProfD";
const FILE_DATABASE = "extensions.sqlite";
diff --git a/toolkit/mozapps/extensions/test/browser/browser-common.ini b/toolkit/mozapps/extensions/test/browser/browser-common.ini
index eaab29f75..3e88833ef 100644
--- a/toolkit/mozapps/extensions/test/browser/browser-common.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini
@@ -39,8 +39,6 @@ skip-if = true # Bug 1093190 - Disabled due to leak
[browser_discovery.js]
skip-if = e10s # Bug ?????? - test times out on try on all platforms, but works locally for markh!
[browser_dragdrop.js]
-skip-if = buildapp == 'mulet'
-[browser_experiments.js]
skip-if = e10s
[browser_list.js]
[browser_metadataTimeout.js]
diff --git a/toolkit/mozapps/installer/find-dupes.py b/toolkit/mozapps/installer/find-dupes.py
index bd0561c97..34ef675f4 100644
--- a/toolkit/mozapps/installer/find-dupes.py
+++ b/toolkit/mozapps/installer/find-dupes.py
@@ -4,15 +4,8 @@
import sys
import hashlib
-import re
-from mozbuild.preprocessor import Preprocessor
-from mozbuild.util import DefinesAction
from mozpack.packager.unpack import UnpackFinder
-from mozpack.files import DeflatedFile
from collections import OrderedDict
-from StringIO import StringIO
-import argparse
-import buildconfig
'''
Find files duplicated in a given packaged directory, independently of its
@@ -20,116 +13,36 @@ package format.
'''
-def normalize_osx_path(p):
- '''
- Strips the first 3 elements of an OSX app path
-
- >>> normalize_osx_path('Nightly.app/foo/bar/baz')
- 'baz'
- '''
- bits = p.split('/')
- if len(bits) > 3 and bits[0].endswith('.app'):
- return '/'.join(bits[3:])
- return p
-
-
-def normalize_l10n_path(p):
- '''
- Normalizes localized paths to en-US
-
- >>> normalize_l10n_path('chrome/es-ES/locale/branding/brand.properties')
- 'chrome/en-US/locale/branding/brand.properties'
- >>> normalize_l10n_path('chrome/fr/locale/fr/browser/aboutHome.dtd')
- 'chrome/en-US/locale/en-US/browser/aboutHome.dtd'
- '''
- # Keep a trailing slash here! e.g. locales like 'br' can transform
- # 'chrome/br/locale/branding/' into 'chrome/en-US/locale/en-USanding/'
- p = re.sub(r'chrome/(\S+)/locale/\1/',
- 'chrome/en-US/locale/en-US/',
- p)
- p = re.sub(r'chrome/(\S+)/locale/',
- 'chrome/en-US/locale/',
- p)
- return p
-
-
-def normalize_path(p):
- return normalize_osx_path(normalize_l10n_path(p))
-
-
-def find_dupes(source, allowed_dupes, bail=True):
- allowed_dupes = set(allowed_dupes)
+def find_dupes(source):
md5s = OrderedDict()
for p, f in UnpackFinder(source):
content = f.open().read()
m = hashlib.md5(content).digest()
- if m not in md5s:
- if isinstance(f, DeflatedFile):
- compressed = f.file.compressed_size
- else:
- compressed = len(content)
- md5s[m] = (len(content), compressed, [])
- md5s[m][2].append(p)
+ if not m in md5s:
+ md5s[m] = (len(content), [])
+ md5s[m][1].append(p)
total = 0
- total_compressed = 0
num_dupes = 0
- unexpected_dupes = []
- for m, (size, compressed, paths) in sorted(md5s.iteritems(),
- key=lambda x: x[1][1]):
+ for m, (size, paths) in md5s.iteritems():
if len(paths) > 1:
- print 'Duplicates %d bytes%s%s:' % (size,
- ' (%d compressed)' % compressed if compressed != size else '',
+ print 'Duplicates %d bytes%s:' % (size,
' (%d times)' % (len(paths) - 1) if len(paths) > 2 else '')
print ''.join(' %s\n' % p for p in paths)
total += (len(paths) - 1) * size
- total_compressed += (len(paths) - 1) * compressed
num_dupes += 1
-
- unexpected_dupes.extend([p for p in paths if normalize_path(p) not in allowed_dupes])
-
if num_dupes:
- print "WARNING: Found %d duplicated files taking %d bytes (%s)" % \
- (num_dupes, total,
- '%d compressed' % total_compressed if total_compressed != total
- else 'uncompressed')
-
- if unexpected_dupes:
- errortype = "ERROR" if bail else "WARNING"
- print "%s: The following duplicated files are not allowed:" % errortype
- print "\n".join(unexpected_dupes)
- if bail:
- sys.exit(1)
+ print "WARNING: Found %d duplicated files taking %d bytes" % \
+ (num_dupes, total) + " (uncompressed)"
def main():
- parser = argparse.ArgumentParser(description='Find duplicate files in directory.')
- parser.add_argument('--warning', '-w', action='store_true',
- help='Only warn about duplicates, do not exit with an error')
- parser.add_argument('--file', '-f', action='append', dest='dupes_files', default=[],
- help='Add exceptions to the duplicate list from this file')
- parser.add_argument('-D', action=DefinesAction)
- parser.add_argument('-U', action='append', default=[])
- parser.add_argument('directory',
- help='The directory to check for duplicates in')
-
- args = parser.parse_args()
-
- allowed_dupes = []
- for filename in args.dupes_files:
- pp = Preprocessor()
- pp.context.update(buildconfig.defines)
- if args.D:
- pp.context.update(args.D)
- for undefine in args.U:
- if undefine in pp.context:
- del pp.context[undefine]
- pp.out = StringIO()
- pp.do_filter('substitution')
- pp.do_include(filename)
- allowed_dupes.extend([line.partition('#')[0].rstrip()
- for line in pp.out.getvalue().splitlines()])
+ if len(sys.argv) != 2:
+ import os
+ print >>sys.stderr, "Usage: %s directory" % \
+ os.path.basename(sys.argv[0])
+ sys.exit(1)
- find_dupes(args.directory, bail=not args.warning, allowed_dupes=allowed_dupes)
+ find_dupes(sys.argv[1])
if __name__ == "__main__":
main()
diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk
index 68247e7df..71a956aa4 100644
--- a/toolkit/mozapps/installer/packager.mk
+++ b/toolkit/mozapps/installer/packager.mk
@@ -54,7 +54,7 @@ stage-package: $(MOZ_PKG_MANIFEST) $(MOZ_PKG_MANIFEST_DEPS)
$(addprefix --unify ,$(UNIFY_DIST)) \
$(MOZ_PKG_MANIFEST) $(DIST) $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)$(if $(MOZ_PKG_MANIFEST),,$(_BINPATH)) \
$(if $(filter omni,$(MOZ_PACKAGER_FORMAT)),$(if $(NON_OMNIJAR_FILES),--non-resource $(NON_OMNIJAR_FILES)))
- $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/find-dupes.py $(DEFINES) $(ACDEFINES) $(MOZ_PKG_DUPEFLAGS) $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)
+ $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/find-dupes.py $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)
ifdef MOZ_PACKAGE_JSSHELL
# Package JavaScript Shell
@echo 'Packaging JavaScript Shell...'
diff --git a/toolkit/mozapps/webextensions/test/browser/browser-common.ini b/toolkit/mozapps/webextensions/test/browser/browser-common.ini
index eda266e2f..83920465b 100644
--- a/toolkit/mozapps/webextensions/test/browser/browser-common.ini
+++ b/toolkit/mozapps/webextensions/test/browser/browser-common.ini
@@ -34,7 +34,6 @@ skip-if = true # Bug 1093190 - Disabled due to leak
[browser_discovery.js]
[browser_dragdrop.js]
skip-if = buildapp == 'mulet'
-[browser_experiments.js]
[browser_list.js]
[browser_metadataTimeout.js]
[browser_searching.js]
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