зеркало из https://github.com/mozilla/gecko-dev.git
Backed out changesets 7f2ddcfe4537 and e88770aa2160 (bug 1171344) for intermittent OSX browser_tabopen_reflows.js failures.
--HG-- rename : browser/base/content/contentSearchUI.css => browser/base/content/searchSuggestionUI.css rename : browser/base/content/contentSearchUI.js => browser/base/content/searchSuggestionUI.js rename : browser/base/content/test/general/browser_contentSearchUI.js => browser/base/content/test/general/browser_searchSuggestionUI.js rename : browser/base/content/test/general/contentSearchUI.html => browser/base/content/test/general/searchSuggestionUI.html rename : browser/base/content/test/general/contentSearchUI.js => browser/base/content/test/general/searchSuggestionUI.js
This commit is contained in:
Родитель
9ddcd2a3b5
Коммит
d7e5192373
2
CLOBBER
2
CLOBBER
|
@ -22,4 +22,4 @@
|
|||
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
|
||||
# don't change CLOBBER for WebIDL changes any more.
|
||||
|
||||
Bug 1171344 requires a clobber due to renaming of a test file.
|
||||
Bug 1178850 requires clobber for Android JNI header changes
|
||||
|
|
|
@ -49,57 +49,78 @@ a {
|
|||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#searchIconAndTextContainer,
|
||||
#searchForm,
|
||||
#snippets {
|
||||
width: 470px;
|
||||
}
|
||||
|
||||
#searchIconAndTextContainer {
|
||||
#searchForm {
|
||||
display: -moz-box;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#searchLogoContainer {
|
||||
display: -moz-box;
|
||||
-moz-box-align: center;
|
||||
padding-top: 2px;
|
||||
-moz-padding-end: 8px;
|
||||
}
|
||||
|
||||
#searchLogoContainer[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#searchEngineLogo {
|
||||
display: inline-block;
|
||||
height: 28px;
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
#searchIcon {
|
||||
border: 1px transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
|
||||
position: absolute;
|
||||
border: 1px solid transparent;
|
||||
-moz-margin-end: 5px;
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
background: url("chrome://browser/skin/magnifier.png") center center no-repeat;
|
||||
background-size: 26px;
|
||||
}
|
||||
|
||||
#searchIcon[active],
|
||||
#searchIcon:hover {
|
||||
background-color: #e9e9e9;
|
||||
border-color: rgb(226, 227, 229);
|
||||
border-radius: 2.5px;
|
||||
}
|
||||
|
||||
html[searchUIConfiguration="oldsearchui"] #searchIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html:not([searchUIConfiguration="oldsearchui"]) #searchText::-moz-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
html:not([searchUIConfiguration="oldsearchui"]) #searchLogoContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#searchText {
|
||||
margin-left: 0;
|
||||
-moz-box-flex: 1;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 34px;
|
||||
padding-right: 8px;
|
||||
padding: 6px 8px;
|
||||
background: hsla(0,0%,100%,.9) padding-box;
|
||||
border: 1px solid;
|
||||
border-radius: 2px 0 0 2px;
|
||||
border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
|
||||
box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
|
||||
0 0 2px hsla(210,65%,9%,.1) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
border-radius: 2.5px 0 0 2.5px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#searchText:-moz-dir(rtl) {
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-radius: 0 2.5px 2.5px 0;
|
||||
}
|
||||
|
||||
#searchText[aria-expanded="true"] {
|
||||
border-radius: 2px 0 0 0;
|
||||
}
|
||||
|
||||
#searchText[aria-expanded="true"]:-moz-dir(rtl) {
|
||||
border-radius: 0 2px 0 0;
|
||||
}
|
||||
|
||||
#searchText[keepfocus],
|
||||
#searchText:focus,
|
||||
#searchText[autofocus] {
|
||||
border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
|
||||
|
@ -107,27 +128,25 @@ a {
|
|||
|
||||
#searchSubmit {
|
||||
-moz-margin-start: -1px;
|
||||
background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
|
||||
padding: 0;
|
||||
background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
|
||||
padding: 0 9px;
|
||||
border: 1px solid;
|
||||
border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
|
||||
border-radius: 0 2px 2px 0;
|
||||
-moz-border-start: 1px solid transparent;
|
||||
border-radius: 0 2.5px 2.5px 0;
|
||||
box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
transition-duration: 150ms;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#searchSubmit:-moz-dir(rtl) {
|
||||
border-radius: 2px 0 0 2px;
|
||||
border-radius: 2.5px 0 0 2.5px;
|
||||
}
|
||||
|
||||
#searchText:focus + #searchSubmit,
|
||||
#searchText[keepfocus] + #searchSubmit,
|
||||
#searchText + #searchSubmit:hover,
|
||||
#searchText[autofocus] + #searchSubmit {
|
||||
border-color: #59b5fc #45a3e7 #3294d5;
|
||||
|
@ -135,16 +154,15 @@ a {
|
|||
}
|
||||
|
||||
#searchText:focus + #searchSubmit,
|
||||
#searchText[keepfocus] + #searchSubmit,
|
||||
#searchText[autofocus] + #searchSubmit {
|
||||
background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#4cb1ff, #1793e5);
|
||||
background-image: linear-gradient(#4cb1ff, #1793e5);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
|
||||
0 0 0 1px hsla(0,0%,100%,.1) inset,
|
||||
0 1px 0 hsla(210,54%,20%,.03);
|
||||
}
|
||||
|
||||
#searchText + #searchSubmit:hover {
|
||||
background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#66bdff, #0d9eff);
|
||||
background-image: linear-gradient(#66bdff, #0d9eff);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
|
||||
0 0 0 1px hsla(0,0%,100%,.1) inset,
|
||||
0 1px 0 hsla(210,54%,20%,.03),
|
||||
|
@ -162,9 +180,9 @@ a {
|
|||
#rightsSnippet {
|
||||
display: block;
|
||||
min-height: 38px;
|
||||
background: 0 center no-repeat;
|
||||
background: 30px center no-repeat;
|
||||
padding: 6px 0;
|
||||
-moz-padding-start: 49px;
|
||||
-moz-padding-start: 79px;
|
||||
}
|
||||
|
||||
#rightsSnippet[hidden] {
|
||||
|
@ -174,7 +192,7 @@ a {
|
|||
#defaultSnippet1:-moz-dir(rtl),
|
||||
#defaultSnippet2:-moz-dir(rtl),
|
||||
#rightsSnippet:-moz-dir(rtl) {
|
||||
background-position: right 0 center;
|
||||
background-position: right 30px center;
|
||||
}
|
||||
|
||||
#defaultSnippet1 {
|
||||
|
@ -229,7 +247,7 @@ body[narrow] #launcher[session] {
|
|||
white-space: normal;
|
||||
background: transparent padding-box;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
border-radius: 2.5px;
|
||||
color: #525c66;
|
||||
font-size: 75%;
|
||||
cursor: pointer;
|
||||
|
@ -378,6 +396,10 @@ body[narrow] #restorePreviousSession::before {
|
|||
background-image: url("chrome://branding/content/about-logo@2x.png");
|
||||
}
|
||||
|
||||
#searchIcon {
|
||||
background-image: url("chrome://browser/skin/magnifier@2x.png");
|
||||
}
|
||||
|
||||
#defaultSnippet1,
|
||||
#defaultSnippet2,
|
||||
#rightsSnippet {
|
||||
|
|
|
@ -4,6 +4,138 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const SEARCH_ENGINES = {
|
||||
"Google": {
|
||||
// This is the "2x" image designed for OS X retina resolution, Windows at 192dpi, etc.;
|
||||
// it will be scaled down as necessary on lower-dpi displays.
|
||||
// This needs to be defined in a single line to keep the JS parser from creating many
|
||||
// intermediate strings in memory. See bug 986672.
|
||||
image: "data:image/png;base64,\
|
||||
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"
|
||||
}
|
||||
};
|
||||
|
||||
// The process of adding a new default snippet involves:
|
||||
// * add a new entity to aboutHome.dtd
|
||||
// * add a <span/> for it in aboutHome.xhtml
|
||||
|
@ -26,7 +158,8 @@ const SNIPPETS_OBJECTSTORE_NAME = "snippets";
|
|||
let gInitialized = false;
|
||||
let gObserver = new MutationObserver(function (mutations) {
|
||||
for (let mutation of mutations) {
|
||||
if (mutation.attributeName == "snippetsVersion") {
|
||||
if (mutation.attributeName == "searchEngineName") {
|
||||
setupSearchEngine();
|
||||
if (!gInitialized) {
|
||||
ensureSnippetsMapThen(loadSnippets);
|
||||
gInitialized = true;
|
||||
|
@ -41,7 +174,6 @@ window.addEventListener("pageshow", function () {
|
|||
// later and may use asynchronous getters.
|
||||
window.gObserver.observe(document.documentElement, { attributes: true });
|
||||
fitToWidth();
|
||||
setupSearch();
|
||||
window.addEventListener("resize", fitToWidth);
|
||||
|
||||
// Ask chrome to update snippets.
|
||||
|
@ -168,13 +300,52 @@ function ensureSnippetsMapThen(aCallback)
|
|||
|
||||
function onSearchSubmit(aEvent)
|
||||
{
|
||||
gContentSearchController.search(aEvent);
|
||||
let searchText = document.getElementById("searchText");
|
||||
let searchTerms = searchText.value;
|
||||
let engineName = document.documentElement.getAttribute("searchEngineName");
|
||||
|
||||
if (engineName && searchTerms.length > 0) {
|
||||
// Send an event that will perform a search and Firefox Health Report will
|
||||
// record that a search from about:home has occurred.
|
||||
let eventData = {
|
||||
engineName: engineName,
|
||||
searchTerms: searchTerms,
|
||||
originalEvent: {
|
||||
target: {
|
||||
ownerDocument: null
|
||||
},
|
||||
shiftKey: aEvent.shiftKey,
|
||||
ctrlKey: aEvent.ctrlKey,
|
||||
metaKey: aEvent.metaKey,
|
||||
altKey: aEvent.altKey,
|
||||
button: aEvent.button,
|
||||
},
|
||||
};
|
||||
|
||||
if (searchText.hasAttribute("selection-index")) {
|
||||
eventData.selection = {
|
||||
index: searchText.getAttribute("selection-index"),
|
||||
kind: searchText.getAttribute("selection-kind")
|
||||
};
|
||||
}
|
||||
|
||||
eventData = JSON.stringify(eventData);
|
||||
|
||||
let event = new CustomEvent("AboutHomeSearchEvent", {detail: eventData});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
gSearchSuggestionController.addInputValueToFormHistory();
|
||||
|
||||
if (aEvent) {
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let gContentSearchController;
|
||||
let gSearchSuggestionController;
|
||||
|
||||
function setupSearch()
|
||||
function setupSearchEngine()
|
||||
{
|
||||
// The "autofocus" attribute doesn't focus the form element
|
||||
// immediately when the element is first drawn, so the
|
||||
|
@ -184,12 +355,29 @@ function setupSearch()
|
|||
searchText.removeEventListener("blur", searchText_onBlur);
|
||||
searchText.removeAttribute("autofocus");
|
||||
});
|
||||
|
||||
let searchEngineName = document.documentElement.getAttribute("searchEngineName");
|
||||
let searchEngineInfo = SEARCH_ENGINES[searchEngineName];
|
||||
let logoElt = document.getElementById("searchEngineLogo");
|
||||
|
||||
if (!gContentSearchController) {
|
||||
gContentSearchController =
|
||||
new ContentSearchUIController(searchText, searchText.parentNode,
|
||||
"abouthome", "homepage");
|
||||
// Add search engine logo.
|
||||
if (searchEngineInfo && searchEngineInfo.image) {
|
||||
logoElt.parentNode.hidden = false;
|
||||
logoElt.src = searchEngineInfo.image;
|
||||
logoElt.alt = searchEngineName;
|
||||
searchText.placeholder = "";
|
||||
}
|
||||
else {
|
||||
logoElt.parentNode.hidden = true;
|
||||
searchText.placeholder = searchEngineName;
|
||||
}
|
||||
|
||||
if (!gSearchSuggestionController) {
|
||||
gSearchSuggestionController =
|
||||
new SearchSuggestionUIController(searchText, searchText.parentNode,
|
||||
onSearchSubmit);
|
||||
}
|
||||
gSearchSuggestionController.engineName = searchEngineName;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,14 +24,14 @@
|
|||
<link rel="icon" type="image/png" id="favicon"
|
||||
href="chrome://branding/content/icon32.png"/>
|
||||
<link rel="stylesheet" type="text/css" media="all"
|
||||
href="chrome://browser/content/contentSearchUI.css"/>
|
||||
href="chrome://browser/content/searchSuggestionUI.css"/>
|
||||
<link rel="stylesheet" type="text/css" media="all" defer="defer"
|
||||
href="chrome://browser/content/abouthome/aboutHome.css"/>
|
||||
|
||||
<script type="text/javascript;version=1.8"
|
||||
src="chrome://browser/content/abouthome/aboutHome.js"/>
|
||||
<script type="text/javascript;version=1.8"
|
||||
src="chrome://browser/content/contentSearchUI.js"/>
|
||||
src="chrome://browser/content/searchSuggestionUI.js"/>
|
||||
</head>
|
||||
|
||||
<body dir="&locale.dir;">
|
||||
|
@ -39,12 +39,14 @@
|
|||
<div id="topSection">
|
||||
<div id="brandLogo"></div>
|
||||
|
||||
<div id="searchIconAndTextContainer">
|
||||
<div id="searchIcon"/>
|
||||
<input type="text" name="q" value="" id="searchText" maxlength="256"
|
||||
aria-label="&contentSearchInput.label;" autofocus="autofocus" dir="auto"/>
|
||||
<input id="searchSubmit" type="button" value="" onclick="onSearchSubmit(event)"
|
||||
aria-label="&contentSearchSubmit.label;"/>
|
||||
<div id="searchContainer">
|
||||
<form name="searchForm" id="searchForm" onsubmit="onSearchSubmit(event)">
|
||||
<div id="searchLogoContainer" hidden="true"><img id="searchEngineLogo"/></div>
|
||||
<button id="searchIcon" type="button" />
|
||||
<input type="text" name="q" value="" id="searchText" maxlength="256"
|
||||
autofocus="autofocus" dir="auto"/>
|
||||
<input id="searchSubmit" type="submit" value="&abouthome.searchEngineButton.label;"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="snippetContainer">
|
||||
|
|
|
@ -3467,8 +3467,9 @@ const BrowserSearch = {
|
|||
if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) {
|
||||
let url = gBrowser.currentURI.spec.toLowerCase();
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
if (url === "about:home" ||
|
||||
(url === "about:newtab" && NewTabUtils.allPages.enabled)) {
|
||||
if (url === "about:home") {
|
||||
AboutHome.focusInput(mm);
|
||||
} else if (url === "about:newtab" && NewTabUtils.allPages.enabled) {
|
||||
ContentSearch.focusInput(mm);
|
||||
} else {
|
||||
openUILinkIn("about:home", "current");
|
||||
|
|
|
@ -1,152 +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/. */
|
||||
|
||||
.contentSearchSuggestionTable {
|
||||
background-color: hsla(0,0%,100%,.99);
|
||||
border: 1px solid hsla(0, 0%, 0%, .2);
|
||||
border-top: none;
|
||||
box-shadow: 0 5px 10px hsla(0, 0%, 0%, .1);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
-moz-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.contentSearchSuggestionsList {
|
||||
border-bottom: 1px solid hsl(0, 0%, 92%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.contentSearchSuggestionTable,
|
||||
.contentSearchSuggestionsList {
|
||||
border-spacing: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.contentSearchHeaderRow,
|
||||
.contentSearchSuggestionRow {
|
||||
margin: 0;
|
||||
max-width: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contentSearchHeaderRow > td > img,
|
||||
.contentSearchSuggestionRow > td > .historyIcon {
|
||||
margin-right: 8px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
|
||||
.contentSearchSuggestionTable .historyIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon");
|
||||
}
|
||||
|
||||
.contentSearchSuggestionRow.selected > td > .historyIcon {
|
||||
background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon-active");
|
||||
}
|
||||
|
||||
.contentSearchHeader > img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contentSearchSuggestionRow.remote > td > .historyIcon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.contentSearchSuggestionRow.selected {
|
||||
background-color: Highlight;
|
||||
color: HighlightText;
|
||||
}
|
||||
|
||||
.contentSearchHeader,
|
||||
.contentSearchSuggestionEntry {
|
||||
margin: 0;
|
||||
max-width: inherit;
|
||||
overflow: hidden;
|
||||
padding: 4px 10px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.contentSearchHeader {
|
||||
background-color: hsl(0, 0%, 97%);
|
||||
color: #666;
|
||||
border-bottom: 1px solid hsl(0, 0%, 92%);
|
||||
}
|
||||
|
||||
.contentSearchSuggestionsContainer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contentSearchSearchWithHeaderSearchText {
|
||||
white-space: pre;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.contentSearchOneOffItem {
|
||||
-moz-appearance: none;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAWCAYAAAABxvaqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gofECQNNVW2/AAAABBJREFUGFdjOHPmzH8GehEA/KpKg9YTf4AAAAAASUVORK5CYII=');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
}
|
||||
|
||||
.contentSearchOneOffItem > img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.contentSearchOneOffItem:not(.last-row) {
|
||||
border-bottom: 1px solid hsl(0, 0%, 92%);
|
||||
}
|
||||
|
||||
.contentSearchOneOffItem.end-of-row {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.contentSearchOneOffItem.selected {
|
||||
background-color: Highlight;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.contentSearchOneOffsTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contentSearchSettingsButton {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-top: 1px solid hsla(0, 0%, 0%, .08);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contentSearchSettingsButton.selected {
|
||||
background-color: hsl(0, 0%, 90%);
|
||||
}
|
||||
|
||||
.contentSearchSettingsButton:active {
|
||||
background-color: hsl(0, 0%, 85%);
|
||||
}
|
|
@ -1,735 +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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.ContentSearchUIController = (function () {
|
||||
|
||||
const MAX_DISPLAYED_SUGGESTIONS = 6;
|
||||
const SUGGESTION_ID_PREFIX = "searchSuggestion";
|
||||
const ONE_OFF_ID_PREFIX = "oneOff";
|
||||
const CSS_URI = "chrome://browser/content/contentSearchUI.css";
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* Creates a new object that manages search suggestions and their UI for a text
|
||||
* box.
|
||||
*
|
||||
* The UI consists of an html:table that's inserted into the DOM after the given
|
||||
* text box and styled so that it appears as a dropdown below the text box.
|
||||
*
|
||||
* @param inputElement
|
||||
* Search suggestions will be based on the text in this text box.
|
||||
* Assumed to be an html:input. xul:textbox is untested but might work.
|
||||
* @param tableParent
|
||||
* The suggestion table is appended as a child to this element. Since
|
||||
* the table is absolutely positioned and its top and left values are set
|
||||
* to be relative to the top and left of the page, either the parent and
|
||||
* all its ancestors should not be positioned elements (i.e., their
|
||||
* positions should be "static"), or the parent's position should be the
|
||||
* top left of the page.
|
||||
* @param healthReportKey
|
||||
* This will be sent with the search data for FHR to record the search.
|
||||
* @param searchPurpose
|
||||
* Sent with search data, see nsISearchEngine.getSubmission.
|
||||
* @param idPrefix
|
||||
* The IDs of elements created by the object will be prefixed with this
|
||||
* string.
|
||||
*/
|
||||
function ContentSearchUIController(inputElement, tableParent, healthReportKey,
|
||||
searchPurpose, idPrefix="") {
|
||||
this.input = inputElement;
|
||||
this._idPrefix = idPrefix;
|
||||
this._healthReportKey = healthReportKey;
|
||||
this._searchPurpose = searchPurpose;
|
||||
|
||||
let tableID = idPrefix + "searchSuggestionTable";
|
||||
this.input.autocomplete = "off";
|
||||
this.input.setAttribute("aria-autocomplete", "true");
|
||||
this.input.setAttribute("aria-controls", tableID);
|
||||
tableParent.appendChild(this._makeTable(tableID));
|
||||
|
||||
this.input.addEventListener("keypress", this);
|
||||
this.input.addEventListener("input", this);
|
||||
this.input.addEventListener("focus", this);
|
||||
this.input.addEventListener("blur", this);
|
||||
window.addEventListener("ContentSearchService", this);
|
||||
|
||||
this._stickyInputValue = "";
|
||||
this._hideSuggestions();
|
||||
|
||||
this._getSearchEngines();
|
||||
this._getStrings();
|
||||
}
|
||||
|
||||
ContentSearchUIController.prototype = {
|
||||
|
||||
// The timeout (ms) of the remote suggestions. Corresponds to
|
||||
// SearchSuggestionController.remoteTimeout. Uses
|
||||
// SearchSuggestionController's default timeout if falsey.
|
||||
remoteTimeout: undefined,
|
||||
_oneOffButtons: [],
|
||||
|
||||
get defaultEngine() {
|
||||
return this._defaultEngine;
|
||||
},
|
||||
|
||||
set defaultEngine(val) {
|
||||
this._defaultEngine = val;
|
||||
this._updateDefaultEngineHeader();
|
||||
|
||||
if (val && document.activeElement == this.input) {
|
||||
this._speculativeConnect();
|
||||
}
|
||||
},
|
||||
|
||||
get engines() {
|
||||
return this._engines;
|
||||
},
|
||||
|
||||
set engines(val) {
|
||||
this._engines = val;
|
||||
this._setUpOneOffButtons();
|
||||
},
|
||||
|
||||
// The selectedIndex is the index of the element with the "selected" class in
|
||||
// the list obtained by concatenating the suggestion rows, one-off buttons, and
|
||||
// search settings button.
|
||||
get selectedIndex() {
|
||||
let allElts = [...this._suggestionsList.children,
|
||||
...this._oneOffButtons,
|
||||
document.getElementById("contentSearchSettingsButton")];
|
||||
for (let i = 0; i < allElts.length; ++i) {
|
||||
let elt = allElts[i];
|
||||
if (elt.classList.contains("selected")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
set selectedIndex(idx) {
|
||||
// Update the table's rows, and the input when there is a selection.
|
||||
this._table.removeAttribute("aria-activedescendant");
|
||||
this.input.removeAttribute("aria-activedescendant");
|
||||
|
||||
let allElts = [...this._suggestionsList.children,
|
||||
...this._oneOffButtons,
|
||||
document.getElementById("contentSearchSettingsButton")];
|
||||
for (let i = 0; i < allElts.length; ++i) {
|
||||
let elt = allElts[i];
|
||||
let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
|
||||
if (i == idx) {
|
||||
elt.classList.add("selected");
|
||||
ariaSelectedElt.setAttribute("aria-selected", "true");
|
||||
this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
|
||||
}
|
||||
else {
|
||||
elt.classList.remove("selected");
|
||||
ariaSelectedElt.setAttribute("aria-selected", "false");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get selectedEngineName() {
|
||||
let selectedElt = this._table.querySelector(".selected");
|
||||
if (selectedElt && selectedElt.engineName) {
|
||||
return selectedElt.engineName;
|
||||
}
|
||||
return this.defaultEngine.name;
|
||||
},
|
||||
|
||||
get numSuggestions() {
|
||||
return this._suggestionsList.children.length;
|
||||
},
|
||||
|
||||
selectAndUpdateInput: function (idx) {
|
||||
this.selectedIndex = idx;
|
||||
let newValue = this.suggestionAtIndex(idx) || this._stickyInputValue;
|
||||
// Setting the input value when the value has not changed commits the current
|
||||
// IME composition, which we don't want to do.
|
||||
if (this.input.value != newValue) {
|
||||
this.input.value = newValue;
|
||||
}
|
||||
this._updateSearchWithHeader();
|
||||
},
|
||||
|
||||
suggestionAtIndex: function (idx) {
|
||||
let row = this._suggestionsList.children[idx];
|
||||
return row ? row.textContent : null;
|
||||
},
|
||||
|
||||
deleteSuggestionAtIndex: function (idx) {
|
||||
// Only form history suggestions can be deleted.
|
||||
if (this.isFormHistorySuggestionAtIndex(idx)) {
|
||||
let suggestionStr = this.suggestionAtIndex(idx);
|
||||
this._sendMsg("RemoveFormHistoryEntry", suggestionStr);
|
||||
this._suggestionsList.children[idx].remove();
|
||||
this.selectAndUpdateInput(-1);
|
||||
}
|
||||
},
|
||||
|
||||
isFormHistorySuggestionAtIndex: function (idx) {
|
||||
let row = this._suggestionsList.children[idx];
|
||||
return row && row.classList.contains("formHistory");
|
||||
},
|
||||
|
||||
addInputValueToFormHistory: function () {
|
||||
this._sendMsg("AddFormHistoryEntry", this.input.value);
|
||||
},
|
||||
|
||||
handleEvent: function (event) {
|
||||
this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
|
||||
},
|
||||
|
||||
_onCommand: function(aEvent) {
|
||||
if (this.selectedIndex == this.numSuggestions + this._oneOffButtons.length) {
|
||||
// Settings button was selected.
|
||||
this._sendMsg("ManageEngines");
|
||||
return;
|
||||
}
|
||||
|
||||
this.search(aEvent);
|
||||
|
||||
if (aEvent) {
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
search: function (aEvent) {
|
||||
if (!this.defaultEngine) {
|
||||
return; // Not initialized yet.
|
||||
}
|
||||
|
||||
let searchText = this.input;
|
||||
let searchTerms;
|
||||
if (this._table.hidden ||
|
||||
aEvent.originalTarget.id == "contentSearchDefaultEngineHeader") {
|
||||
searchTerms = searchText.value;
|
||||
}
|
||||
else {
|
||||
searchTerms = this.suggestionAtIndex(this.selectedIndex) || searchText.value;
|
||||
}
|
||||
// Send an event that will perform a search and Firefox Health Report will
|
||||
// record that a search from the healthReportKey passed to the constructor.
|
||||
let eventData = {
|
||||
engineName: this.selectedEngineName,
|
||||
searchString: searchTerms,
|
||||
healthReportKey: this._healthReportKey,
|
||||
searchPurpose: this._searchPurpose,
|
||||
originalEvent: {
|
||||
shiftKey: aEvent.shiftKey,
|
||||
ctrlKey: aEvent.ctrlKey,
|
||||
metaKey: aEvent.metaKey,
|
||||
altKey: aEvent.altKey,
|
||||
button: aEvent.button,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.suggestionAtIndex(this.selectedIndex)) {
|
||||
eventData.selection = {
|
||||
index: this.selectedIndex,
|
||||
kind: aEvent instanceof MouseEvent ? "mouse" :
|
||||
aEvent instanceof KeyboardEvent ? "key" : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
this._sendMsg("Search", eventData);
|
||||
this.addInputValueToFormHistory();
|
||||
},
|
||||
|
||||
_onInput: function () {
|
||||
if (!this.input.value) {
|
||||
this._stickyInputValue = "";
|
||||
this._hideSuggestions();
|
||||
}
|
||||
else if (this.input.value != this._stickyInputValue) {
|
||||
// Only fetch new suggestions if the input value has changed.
|
||||
this._getSuggestions();
|
||||
this.selectAndUpdateInput(-1);
|
||||
}
|
||||
this._updateSearchWithHeader();
|
||||
},
|
||||
|
||||
_onKeypress: function (event) {
|
||||
let selectedIndexDelta = 0;
|
||||
switch (event.keyCode) {
|
||||
case event.DOM_VK_UP:
|
||||
if (!this._table.hidden) {
|
||||
selectedIndexDelta = -1;
|
||||
}
|
||||
break;
|
||||
case event.DOM_VK_DOWN:
|
||||
if (this._table.hidden) {
|
||||
this._getSuggestions();
|
||||
}
|
||||
else {
|
||||
selectedIndexDelta = 1;
|
||||
}
|
||||
break;
|
||||
case event.DOM_VK_RIGHT:
|
||||
// Allow normal caret movement until the caret is at the end of the input.
|
||||
if (this.input.selectionStart != this.input.selectionEnd ||
|
||||
this.input.selectionEnd != this.input.value.length) {
|
||||
return;
|
||||
}
|
||||
if (this.numSuggestions && this.selectedIndex >= 0 &&
|
||||
this.selectedIndex < this.numSuggestions) {
|
||||
this.input.value = this.suggestionAtIndex(this.selectedIndex);
|
||||
this.input.setAttribute("selection-index", this.selectedIndex);
|
||||
this.input.setAttribute("selection-kind", "key");
|
||||
} else {
|
||||
// If we didn't select anything, make sure to remove the attributes
|
||||
// in case they were populated last time.
|
||||
this.input.removeAttribute("selection-index");
|
||||
this.input.removeAttribute("selection-kind");
|
||||
}
|
||||
this._stickyInputValue = this.input.value;
|
||||
this._hideSuggestions();
|
||||
break;
|
||||
case event.DOM_VK_RETURN:
|
||||
this._onCommand(event);
|
||||
break;
|
||||
case event.DOM_VK_DELETE:
|
||||
if (this.selectedIndex >= 0) {
|
||||
this.deleteSuggestionAtIndex(this.selectedIndex);
|
||||
}
|
||||
break;
|
||||
case event.DOM_VK_ESCAPE:
|
||||
if (!this._table.hidden) {
|
||||
this._hideSuggestions();
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIndexDelta) {
|
||||
// Update the selection.
|
||||
let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
|
||||
if (newSelectedIndex < -1) {
|
||||
newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
|
||||
}
|
||||
else if (this.numSuggestions + this._oneOffButtons.length < newSelectedIndex) {
|
||||
newSelectedIndex = -1;
|
||||
}
|
||||
this.selectAndUpdateInput(newSelectedIndex);
|
||||
|
||||
// Prevent the input's caret from moving.
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
_onFocus: function () {
|
||||
if (this._mousedown) {
|
||||
return;
|
||||
}
|
||||
// When the input box loses focus to something in our table, we refocus it
|
||||
// immediately. This causes the focus highlight to flicker, so we set a
|
||||
// custom attribute which consumers should use for focus highlighting. This
|
||||
// attribute is removed only when we do not immediately refocus the input
|
||||
// box, thus eliminating flicker.
|
||||
this.input.setAttribute("keepfocus", "true");
|
||||
this._speculativeConnect();
|
||||
},
|
||||
|
||||
_onBlur: function () {
|
||||
if (this._mousedown) {
|
||||
// At this point, this.input has lost focus, but a new element has not yet
|
||||
// received it. If we re-focus this.input directly, the new element will
|
||||
// steal focus immediately, so we queue it instead.
|
||||
setTimeout(() => this.input.focus(), 0);
|
||||
return;
|
||||
}
|
||||
this.input.removeAttribute("keepfocus");
|
||||
this._hideSuggestions();
|
||||
},
|
||||
|
||||
_onMousemove: function (event) {
|
||||
this.selectedIndex = this._indexOfTableItem(event.target);
|
||||
},
|
||||
|
||||
_onMouseup: function (event) {
|
||||
if (event.button == 2) {
|
||||
return;
|
||||
}
|
||||
this._onCommand(event);
|
||||
},
|
||||
|
||||
_onClick: function (event) {
|
||||
this._onMouseup(event);
|
||||
},
|
||||
|
||||
_onContentSearchService: function (event) {
|
||||
let methodName = "_onMsg" + event.detail.type;
|
||||
if (methodName in this) {
|
||||
this[methodName](event.detail.data);
|
||||
}
|
||||
},
|
||||
|
||||
_onMsgFocusInput: function (event) {
|
||||
this.input.focus();
|
||||
},
|
||||
|
||||
_onMsgSuggestions: function (suggestions) {
|
||||
// Ignore the suggestions if their search string or engine doesn't match
|
||||
// ours. Due to the async nature of message passing, this can easily happen
|
||||
// when the user types quickly.
|
||||
if (this._stickyInputValue != suggestions.searchString ||
|
||||
this.defaultEngine.name != suggestions.engineName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearSuggestionRows();
|
||||
|
||||
// Position and size the table.
|
||||
let { left } = this.input.getBoundingClientRect();
|
||||
this._table.style.top = this.input.offsetHeight + "px";
|
||||
this._table.style.minWidth = this.input.offsetWidth + "px";
|
||||
this._table.style.maxWidth = (window.innerWidth - left - 40) + "px";
|
||||
|
||||
// Add the suggestions to the table.
|
||||
let searchWords =
|
||||
new Set(suggestions.searchString.trim().toLowerCase().split(/\s+/));
|
||||
for (let i = 0; i < MAX_DISPLAYED_SUGGESTIONS; i++) {
|
||||
let type, idx;
|
||||
if (i < suggestions.formHistory.length) {
|
||||
[type, idx] = ["formHistory", i];
|
||||
}
|
||||
else {
|
||||
let j = i - suggestions.formHistory.length;
|
||||
if (j < suggestions.remote.length) {
|
||||
[type, idx] = ["remote", j];
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._suggestionsList.appendChild(
|
||||
this._makeTableRow(type, suggestions[type][idx], i, searchWords));
|
||||
}
|
||||
|
||||
if (this._table.hidden) {
|
||||
this.selectedIndex = -1;
|
||||
this._table.hidden = false;
|
||||
this.input.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
},
|
||||
|
||||
_onMsgState: function (state) {
|
||||
this.defaultEngine = {
|
||||
name: state.currentEngine.name,
|
||||
icon: this._getFaviconURIFromBuffer(state.currentEngine.iconBuffer),
|
||||
};
|
||||
this.engines = state.engines;
|
||||
},
|
||||
|
||||
_onMsgCurrentState: function (state) {
|
||||
this._onMsgState(state);
|
||||
},
|
||||
|
||||
_onMsgCurrentEngine: function (engine) {
|
||||
this.defaultEngine = {
|
||||
name: engine.name,
|
||||
icon: this._getFaviconURIFromBuffer(engine.iconBuffer),
|
||||
};
|
||||
this._setUpOneOffButtons();
|
||||
},
|
||||
|
||||
_onMsgStrings: function (strings) {
|
||||
this._strings = strings;
|
||||
this._updateDefaultEngineHeader();
|
||||
this._updateSearchWithHeader();
|
||||
},
|
||||
|
||||
_updateDefaultEngineHeader: function () {
|
||||
let header = document.getElementById("contentSearchDefaultEngineHeader");
|
||||
if (this.defaultEngine.icon) {
|
||||
header.firstChild.setAttribute("src", this.defaultEngine.icon);
|
||||
}
|
||||
if (!this._strings) {
|
||||
return;
|
||||
}
|
||||
while (header.firstChild.nextSibling) {
|
||||
header.firstChild.nextSibling.remove();
|
||||
}
|
||||
header.appendChild(document.createTextNode(
|
||||
this._strings.searchHeader.replace("%S", this.defaultEngine.name)));
|
||||
},
|
||||
|
||||
_updateSearchWithHeader: function () {
|
||||
if (!this._strings) {
|
||||
return;
|
||||
}
|
||||
let searchWithHeader = document.getElementById("contentSearchSearchWithHeader");
|
||||
while (searchWithHeader.firstChild) {
|
||||
searchWithHeader.firstChild.remove();
|
||||
}
|
||||
if (this.input.value) {
|
||||
searchWithHeader.appendChild(document.createTextNode(this._strings.searchFor));
|
||||
let span = document.createElementNS(HTML_NS, "span");
|
||||
span.setAttribute("class", "contentSearchSearchWithHeaderSearchText");
|
||||
span.appendChild(document.createTextNode(" " + this.input.value + " "));
|
||||
searchWithHeader.appendChild(span);
|
||||
searchWithHeader.appendChild(document.createTextNode(this._strings.searchWith));
|
||||
return;
|
||||
}
|
||||
searchWithHeader.appendChild(document.createTextNode(this._strings.searchWithHeader));
|
||||
},
|
||||
|
||||
_speculativeConnect: function () {
|
||||
if (this.defaultEngine) {
|
||||
this._sendMsg("SpeculativeConnect", this.defaultEngine.name);
|
||||
}
|
||||
},
|
||||
|
||||
_makeTableRow: function (type, suggestionStr, currentRow, searchWords) {
|
||||
let row = document.createElementNS(HTML_NS, "tr");
|
||||
row.dir = "auto";
|
||||
row.classList.add("contentSearchSuggestionRow");
|
||||
row.classList.add(type);
|
||||
row.setAttribute("role", "presentation");
|
||||
row.addEventListener("mousemove", this);
|
||||
row.addEventListener("mouseup", this);
|
||||
|
||||
let entry = document.createElementNS(HTML_NS, "td");
|
||||
let img = document.createElementNS(HTML_NS, "div");
|
||||
img.setAttribute("class", "historyIcon");
|
||||
entry.appendChild(img);
|
||||
entry.classList.add("contentSearchSuggestionEntry");
|
||||
entry.setAttribute("role", "option");
|
||||
entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow;
|
||||
entry.setAttribute("aria-selected", "false");
|
||||
|
||||
let suggestionWords = suggestionStr.trim().toLowerCase().split(/\s+/);
|
||||
for (let i = 0; i < suggestionWords.length; i++) {
|
||||
let word = suggestionWords[i];
|
||||
let wordSpan = document.createElementNS(HTML_NS, "span");
|
||||
if (searchWords.has(word)) {
|
||||
wordSpan.classList.add("typed");
|
||||
}
|
||||
wordSpan.textContent = word;
|
||||
entry.appendChild(wordSpan);
|
||||
if (i < suggestionWords.length - 1) {
|
||||
entry.appendChild(document.createTextNode(" "));
|
||||
}
|
||||
}
|
||||
|
||||
row.appendChild(entry);
|
||||
return row;
|
||||
},
|
||||
|
||||
// Converts favicon array buffer into data URI of the right size and dpi.
|
||||
_getFaviconURIFromBuffer: function (buffer) {
|
||||
let blob = new Blob([buffer]);
|
||||
let dpiSize = Math.round(16 * window.devicePixelRatio);
|
||||
let sizeStr = dpiSize + "," + dpiSize;
|
||||
return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
|
||||
},
|
||||
|
||||
_getSearchEngines: function () {
|
||||
this._sendMsg("GetState");
|
||||
},
|
||||
|
||||
_getStrings: function () {
|
||||
this._sendMsg("GetStrings");
|
||||
},
|
||||
|
||||
_getSuggestions: function () {
|
||||
this._stickyInputValue = this.input.value;
|
||||
if (this.defaultEngine) {
|
||||
this._sendMsg("GetSuggestions", {
|
||||
engineName: this.defaultEngine.name,
|
||||
searchString: this.input.value,
|
||||
remoteTimeout: this.remoteTimeout,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_clearSuggestionRows: function() {
|
||||
while (this._suggestionsList.firstElementChild) {
|
||||
this._suggestionsList.firstElementChild.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_hideSuggestions: function () {
|
||||
this.input.setAttribute("aria-expanded", "false");
|
||||
this._table.hidden = true;
|
||||
},
|
||||
|
||||
_indexOfTableItem: function (elt) {
|
||||
if (elt.classList.contains("contentSearchOneOffItem")) {
|
||||
return this.numSuggestions + this._oneOffButtons.indexOf(elt);
|
||||
}
|
||||
if (elt.classList.contains("contentSearchSettingsButton")) {
|
||||
return this.numSuggestions + this._oneOffButtons.length;
|
||||
}
|
||||
while (elt && elt.localName != "tr") {
|
||||
elt = elt.parentNode;
|
||||
}
|
||||
if (!elt) {
|
||||
throw new Error("Element is not a row");
|
||||
}
|
||||
return elt.rowIndex;
|
||||
},
|
||||
|
||||
_makeTable: function (id) {
|
||||
this._table = document.createElementNS(HTML_NS, "table");
|
||||
this._table.id = id;
|
||||
this._table.hidden = true;
|
||||
this._table.classList.add("contentSearchSuggestionTable");
|
||||
this._table.setAttribute("role", "presentation");
|
||||
|
||||
// When the search input box loses focus, we want to immediately give focus
|
||||
// back to it if the blur was because the user clicked somewhere in the table.
|
||||
// onBlur uses the _mousedown flag to detect this.
|
||||
this._table.addEventListener("mousedown", () => { this._mousedown = true; });
|
||||
document.addEventListener("mouseup", () => { delete this._mousedown; });
|
||||
|
||||
// Deselect the selected element on mouseout if it wasn't a suggestion.
|
||||
this._table.addEventListener("mouseout", () => {
|
||||
if (this.selectedIndex >= this.numSuggestions) {
|
||||
this.selectAndUpdateInput(-1);
|
||||
}
|
||||
});
|
||||
|
||||
// If a search is loaded in the same tab, ensure the suggestions dropdown
|
||||
// is hidden immediately when the page starts loading and not when it first
|
||||
// appears, in order to provide timely feedback to the user.
|
||||
window.addEventListener("beforeunload", () => { this._hideSuggestions(); });
|
||||
|
||||
let headerRow = document.createElementNS(HTML_NS, "tr");
|
||||
let header = document.createElementNS(HTML_NS, "td");
|
||||
headerRow.setAttribute("class", "contentSearchHeaderRow");
|
||||
header.setAttribute("class", "contentSearchHeader");
|
||||
let img = document.createElementNS(HTML_NS, "img");
|
||||
img.setAttribute("src", "chrome://browser/skin/search-engine-placeholder.png");
|
||||
header.appendChild(img);
|
||||
header.id = "contentSearchDefaultEngineHeader";
|
||||
headerRow.appendChild(header);
|
||||
headerRow.addEventListener("click", this);
|
||||
this._table.appendChild(headerRow);
|
||||
|
||||
let row = document.createElementNS(HTML_NS, "tr");
|
||||
row.setAttribute("class", "contentSearchSuggestionsContainer");
|
||||
let cell = document.createElementNS(HTML_NS, "td");
|
||||
cell.setAttribute("class", "contentSearchSuggestionsContainer");
|
||||
this._suggestionsList = document.createElementNS(HTML_NS, "table");
|
||||
this._suggestionsList.setAttribute("class", "contentSearchSuggestionsList");
|
||||
cell.appendChild(this._suggestionsList);
|
||||
row.appendChild(cell);
|
||||
this._table.appendChild(row);
|
||||
this._suggestionsList.setAttribute("role", "listbox");
|
||||
|
||||
this._oneOffsTable = document.createElementNS(HTML_NS, "table");
|
||||
this._oneOffsTable.setAttribute("class", "contentSearchOneOffsTable");
|
||||
this._oneOffsTable.classList.add("contentSearchSuggestionsContainer");
|
||||
this._oneOffsTable.setAttribute("role", "group");
|
||||
this._table.appendChild(this._oneOffsTable);
|
||||
|
||||
headerRow = document.createElementNS(HTML_NS, "tr");
|
||||
header = document.createElementNS(HTML_NS, "td");
|
||||
headerRow.setAttribute("class", "contentSearchHeaderRow");
|
||||
header.setAttribute("class", "contentSearchHeader");
|
||||
headerRow.appendChild(header);
|
||||
header.id = "contentSearchSearchWithHeader";
|
||||
this._oneOffsTable.appendChild(headerRow);
|
||||
|
||||
let button = document.createElementNS(HTML_NS, "button");
|
||||
button.appendChild(document.createTextNode("Change Search Settings"));
|
||||
button.setAttribute("class", "contentSearchSettingsButton");
|
||||
button.classList.add("contentSearchHeaderRow");
|
||||
button.classList.add("contentSearchHeader");
|
||||
button.id = "contentSearchSettingsButton";
|
||||
button.addEventListener("click", this);
|
||||
button.addEventListener("mousemove", this);
|
||||
this._table.appendChild(button);
|
||||
|
||||
return this._table;
|
||||
},
|
||||
|
||||
_setUpOneOffButtons: function () {
|
||||
// Sometimes we receive a CurrentEngine message from the ContentSearch service
|
||||
// before we've received a State message - i.e. before we have our engines.
|
||||
if (!this._engines) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (this._oneOffsTable.firstChild.nextSibling) {
|
||||
this._oneOffsTable.firstChild.nextSibling.remove();
|
||||
}
|
||||
|
||||
this._oneOffButtons = [];
|
||||
|
||||
let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name);
|
||||
if (!engines.length) {
|
||||
this._oneOffsTable.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const kDefaultButtonWidth = 49; // 48px + 1px border.
|
||||
let rowWidth = this.input.offsetWidth - 2; // 2px border.
|
||||
let enginesPerRow = Math.floor(rowWidth / kDefaultButtonWidth);
|
||||
let buttonWidth = Math.floor(rowWidth / enginesPerRow);
|
||||
|
||||
let row = document.createElementNS(HTML_NS, "tr");
|
||||
let cell = document.createElementNS(HTML_NS, "td");
|
||||
row.setAttribute("class", "contentSearchSuggestionsContainer");
|
||||
cell.setAttribute("class", "contentSearchSuggestionsContainer");
|
||||
|
||||
for (let i = 0; i < engines.length; ++i) {
|
||||
let engine = engines[i];
|
||||
if (i > 0 && i % enginesPerRow == 0) {
|
||||
row.appendChild(cell);
|
||||
this._oneOffsTable.appendChild(row);
|
||||
row = document.createElementNS(HTML_NS, "tr");
|
||||
cell = document.createElementNS(HTML_NS, "td");
|
||||
row.setAttribute("class", "contentSearchSuggestionsContainer");
|
||||
cell.setAttribute("class", "contentSearchSuggestionsContainer");
|
||||
}
|
||||
let button = document.createElementNS(HTML_NS, "button");
|
||||
button.setAttribute("class", "contentSearchOneOffItem");
|
||||
let img = document.createElementNS(HTML_NS, "img");
|
||||
let uri = "chrome://browser/skin/search-engine-placeholder.png";
|
||||
if (engine.iconBuffer) {
|
||||
uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
|
||||
}
|
||||
img.setAttribute("src", uri);
|
||||
button.appendChild(img);
|
||||
button.style.width = buttonWidth + "px";
|
||||
button.setAttribute("title", engine.name);
|
||||
|
||||
button.engineName = engine.name;
|
||||
button.addEventListener("click", this);
|
||||
button.addEventListener("mousemove", this);
|
||||
|
||||
if (engines.length - i <= enginesPerRow - (i % enginesPerRow)) {
|
||||
button.classList.add("last-row");
|
||||
}
|
||||
|
||||
if ((i + 1) % enginesPerRow == 0) {
|
||||
button.classList.add("end-of-row");
|
||||
}
|
||||
|
||||
button.id = ONE_OFF_ID_PREFIX + i;
|
||||
cell.appendChild(button);
|
||||
this._oneOffButtons.push(button);
|
||||
}
|
||||
row.appendChild(cell);
|
||||
this._oneOffsTable.appendChild(row);
|
||||
this._oneOffsTable.hidden = false;
|
||||
},
|
||||
|
||||
_sendMsg: function (type, data=null) {
|
||||
dispatchEvent(new CustomEvent("ContentSearchClient", {
|
||||
detail: {
|
||||
type: type,
|
||||
data: data,
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
return ContentSearchUIController;
|
||||
})();
|
|
@ -330,6 +330,7 @@ input[type=button] {
|
|||
#newtab-search-container {
|
||||
display: -moz-box;
|
||||
position: relative;
|
||||
-moz-box-align: center;
|
||||
-moz-box-pack: center;
|
||||
}
|
||||
|
||||
|
@ -340,76 +341,104 @@ input[type=button] {
|
|||
|
||||
#newtab-search-form {
|
||||
display: -moz-box;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
-moz-box-flex: 1;
|
||||
-moz-box-orient: horizontal;
|
||||
-moz-box-align: center;
|
||||
height: 44px; /* 32 + 6 logo top "padding" + 6 logo bottom "padding" */
|
||||
margin: 26px 20px 10px; /* top: 32 - 6 search form top "padding", bottom: 32 - 16 tiles top margin - 6 logo bottom "padding" */
|
||||
max-width: 600px; /* 2 * (290 cell width + 10 cell margin) */
|
||||
}
|
||||
|
||||
#newtab-search-icon {
|
||||
border: 1px transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
|
||||
position: absolute;
|
||||
#newtab-search-logo {
|
||||
display: -moz-box;
|
||||
width: 38px;
|
||||
height: 38px; /* 26 image height + 6 top "padding" + 6 bottom "padding" */
|
||||
border: 1px solid transparent;
|
||||
-moz-margin-end: 8px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
|
||||
#newtab-search-logo.magnifier {
|
||||
width: 38px; /* 26 image width + 6 left "padding" + 6 right "padding" */
|
||||
-moz-margin-end: 5px;
|
||||
background-size: 26px;
|
||||
background-image: url("chrome://browser/skin/magnifier.png");
|
||||
}
|
||||
|
||||
@media not all and (max-resolution: 1dppx) {
|
||||
#newtab-search-logo.magnifier {
|
||||
background-image: url("chrome://browser/skin/magnifier@2x.png");
|
||||
}
|
||||
}
|
||||
|
||||
#newtab-search-logo[type="logo"] {
|
||||
background-size: 65px 26px;
|
||||
width: 77px; /* 65 image width + 6 left "padding" + 6 right "padding" */
|
||||
}
|
||||
|
||||
#newtab-search-logo[type="favicon"] {
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
#newtab-search-logo[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#newtab-search-logo[active],
|
||||
#newtab-search-logo:hover {
|
||||
background-color: #e9e9e9;
|
||||
border: 1px solid rgb(226, 227, 229);
|
||||
border-radius: 2.5px;
|
||||
}
|
||||
|
||||
#newtab-search-text {
|
||||
height: 38px; /* same height as #newtab-search-logo */
|
||||
-moz-box-flex: 1;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 34px;
|
||||
padding-right: 8px;
|
||||
|
||||
padding: 0 8px;
|
||||
background: hsla(0,0%,100%,.9) padding-box;
|
||||
border: 1px solid;
|
||||
border-spacing: 0;
|
||||
border-radius: 2px 0 0 2px;
|
||||
border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
|
||||
box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
|
||||
0 0 2px hsla(210,65%,9%,.1) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
border-radius: 2.5px 0 0 2.5px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#newtab-search-text:-moz-dir(rtl) {
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-radius: 0 2.5px 2.5px 0;
|
||||
}
|
||||
|
||||
#newtab-search-text[aria-expanded="true"] {
|
||||
border-radius: 2px 0 0 0;
|
||||
}
|
||||
|
||||
#newtab-search-text[aria-expanded="true"]:-moz-dir(rtl) {
|
||||
border-radius: 0 2px 0 0;
|
||||
}
|
||||
|
||||
#newtab-search-text[keepfocus],
|
||||
#newtab-search-text:focus,
|
||||
#newtab-search-text[autofocus] {
|
||||
border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
|
||||
}
|
||||
|
||||
#newtab-search-submit {
|
||||
height: 38px; /* same height as #newtab-search-logo */
|
||||
font-size: 13px !important;
|
||||
|
||||
-moz-margin-start: -1px;
|
||||
background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
|
||||
padding: 0;
|
||||
background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
|
||||
padding: 0 9px;
|
||||
border: 1px solid;
|
||||
border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
|
||||
border-radius: 0 2px 2px 0;
|
||||
-moz-border-start: 1px solid transparent;
|
||||
border-radius: 0 2.5px 2.5px 0;
|
||||
box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
transition-duration: 150ms;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#newtab-search-submit:-moz-dir(rtl) {
|
||||
border-radius: 2px 0 0 2px;
|
||||
border-radius: 2.5px 0 0 2.5px;
|
||||
}
|
||||
|
||||
#newtab-search-text:focus + #newtab-search-submit,
|
||||
|
@ -420,16 +449,15 @@ input[type=button] {
|
|||
}
|
||||
|
||||
#newtab-search-text:focus + #newtab-search-submit,
|
||||
#newtab-search-text[keepfocus] + #newtab-search-submit,
|
||||
#newtab-search-text[autofocus] + #newtab-search-submit {
|
||||
background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#4cb1ff, #1793e5);
|
||||
background-image: linear-gradient(#4cb1ff, #1793e5);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
|
||||
0 0 0 1px hsla(0,0%,100%,.1) inset,
|
||||
0 1px 0 hsla(210,54%,20%,.03);
|
||||
}
|
||||
|
||||
#newtab-search-text + #newtab-search-submit:hover {
|
||||
background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#4cb1ff, #1793e5);
|
||||
background-image: linear-gradient(#66bdff, #0d9eff);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
|
||||
0 0 0 1px hsla(0,0%,100%,.1) inset,
|
||||
0 1px 0 hsla(210,54%,20%,.03),
|
||||
|
@ -516,11 +544,14 @@ input[type=button] {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
#newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
#newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent,
|
||||
#newtab-search-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.newtab-customize-panel-item {
|
||||
.newtab-customize-panel-item,
|
||||
.newtab-search-panel-engine,
|
||||
#newtab-search-manage {
|
||||
line-height: 25px;
|
||||
padding: 15px;
|
||||
-moz-padding-start: 40px;
|
||||
|
@ -529,12 +560,22 @@ input[type=button] {
|
|||
max-width: 300px;
|
||||
}
|
||||
|
||||
.newtab-customize-panel-item:not(:first-child) {
|
||||
.newtab-customize-panel-item:not(:first-child),
|
||||
.newtab-search-panel-engine {
|
||||
border-top: 1px solid threedshadow;
|
||||
}
|
||||
|
||||
.newtab-search-panel-engine > image {
|
||||
-moz-margin-end: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
|
||||
}
|
||||
|
||||
.newtab-customize-panel-subitem > label,
|
||||
.newtab-customize-panel-item > label,
|
||||
.newtab-search-panel-engine > label,
|
||||
#newtab-search-manage > label,
|
||||
.newtab-customize-complex-option {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -584,7 +625,8 @@ input[type=button] {
|
|||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.newtab-customize-panel-item[selected] {
|
||||
.newtab-customize-panel-item[selected],
|
||||
.newtab-search-panel-engine[selected] {
|
||||
background: url("chrome://global/skin/menu/shared-menu-check-active.svg") no-repeat transparent;
|
||||
background-size: 16px 16px;
|
||||
background-position: 15px 15px;
|
||||
|
@ -629,7 +671,7 @@ input[type=button] {
|
|||
border-top: 1px solid threedshadow;
|
||||
}
|
||||
|
||||
.contentSearchSuggestionTable {
|
||||
.searchSuggestionTable {
|
||||
font: message-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/contentSearchUI.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/searchSuggestionUI.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE window [
|
||||
<!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
|
||||
%newTabDTD;
|
||||
<!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd">
|
||||
%searchBarDTD;
|
||||
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
|
||||
%browserDTD;
|
||||
]>
|
||||
|
@ -20,6 +22,13 @@
|
|||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
title="&newtab.pageTitle;">
|
||||
|
||||
<xul:panel id="newtab-search-panel" orient="vertical" type="arrow"
|
||||
noautohide="true" hidden="true">
|
||||
<xul:hbox id="newtab-search-manage">
|
||||
<xul:label>&changeSearchSettings.button;</xul:label>
|
||||
</xul:hbox>
|
||||
</xul:panel>
|
||||
|
||||
<div class="newtab-customize-panel-container">
|
||||
<div id="newtab-customize-panel" orient="vertical">
|
||||
<div id="newtab-customize-panel-anchor"></div>
|
||||
|
@ -99,13 +108,13 @@
|
|||
</div>
|
||||
|
||||
<div id="newtab-search-container">
|
||||
<div id="newtab-search-form">
|
||||
<div id="newtab-search-icon"/>
|
||||
<form id="newtab-search-form" name="searchForm">
|
||||
<div id="newtab-search-logo"/>
|
||||
<input type="text" name="q" value="" id="newtab-search-text"
|
||||
aria-label="&contentSearchInput.label;" maxlength="256" dir="auto"/>
|
||||
<input id="newtab-search-submit" type="button" value=""
|
||||
aria-label="&contentSearchSubmit.label;"/>
|
||||
</div>
|
||||
maxlength="256" dir="auto"/>
|
||||
<input id="newtab-search-submit" type="submit"
|
||||
value="&searchEndCap.label;"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="newtab-horizontal-margin">
|
||||
|
@ -124,7 +133,7 @@
|
|||
</div>
|
||||
|
||||
<xul:script type="text/javascript;version=1.8"
|
||||
src="chrome://browser/content/contentSearchUI.js"/>
|
||||
src="chrome://browser/content/searchSuggestionUI.js"/>
|
||||
<xul:script type="text/javascript;version=1.8"
|
||||
src="chrome://browser/content/newtab/newTab.js"/>
|
||||
</xul:window>
|
||||
|
|
|
@ -5,11 +5,257 @@
|
|||
#endif
|
||||
|
||||
let gSearch = {
|
||||
|
||||
currentEngineName: null,
|
||||
|
||||
get useNewUI() {
|
||||
let newUI = Services.prefs.getBoolPref("browser.search.showOneOffButtons");
|
||||
delete this.useNewUI;
|
||||
this.useNewUI = newUI;
|
||||
return newUI;
|
||||
},
|
||||
|
||||
init: function () {
|
||||
document.getElementById("newtab-search-submit")
|
||||
.addEventListener("click", e => this._contentSearchController.search(e));
|
||||
let textbox = document.getElementById("newtab-search-text");
|
||||
this._contentSearchController =
|
||||
new ContentSearchUIController(textbox, textbox.parentNode, "newtab", "newtab");
|
||||
for (let idSuffix of this._nodeIDSuffixes) {
|
||||
this._nodes[idSuffix] =
|
||||
document.getElementById("newtab-search-" + idSuffix);
|
||||
}
|
||||
|
||||
if (this.useNewUI) {
|
||||
this._nodes.logo.classList.add("magnifier");
|
||||
}
|
||||
|
||||
window.addEventListener("ContentSearchService", this);
|
||||
this._send("GetState");
|
||||
},
|
||||
|
||||
showPanel: function () {
|
||||
let panel = this._nodes.panel;
|
||||
let logo = this._nodes.logo;
|
||||
panel.hidden = false;
|
||||
panel.openPopup(logo);
|
||||
logo.setAttribute("active", "true");
|
||||
panel.addEventListener("popuphidden", function onHidden() {
|
||||
panel.removeEventListener("popuphidden", onHidden);
|
||||
panel.hidden = true;
|
||||
logo.removeAttribute("active");
|
||||
});
|
||||
},
|
||||
|
||||
search: function (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
let searchText = this._nodes.text;
|
||||
let searchStr = searchText.value;
|
||||
if (this.currentEngineName && searchStr.length) {
|
||||
let useNewTab = event && event.button == 1;
|
||||
let eventData = {
|
||||
engineName: this.currentEngineName,
|
||||
searchString: searchStr,
|
||||
whence: "newtab",
|
||||
originalEvent: {
|
||||
target: {
|
||||
ownerDocument: null
|
||||
},
|
||||
shiftKey: event.shiftKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
altKey: event.altKey,
|
||||
button: event.button,
|
||||
},
|
||||
}
|
||||
|
||||
if (searchText.hasAttribute("selection-index")) {
|
||||
eventData.selection = {
|
||||
index: searchText.getAttribute("selection-index"),
|
||||
kind: searchText.getAttribute("selection-kind")
|
||||
};
|
||||
}
|
||||
|
||||
this._send("Search", eventData);
|
||||
}
|
||||
this._suggestionController.addInputValueToFormHistory();
|
||||
},
|
||||
|
||||
manageEngines: function () {
|
||||
this._nodes.panel.hidePopup();
|
||||
this._send("ManageEngines");
|
||||
},
|
||||
|
||||
handleEvent: function (event) {
|
||||
let methodName = "on" + event.detail.type;
|
||||
if (this.hasOwnProperty(methodName)) {
|
||||
this[methodName](event.detail.data);
|
||||
}
|
||||
},
|
||||
|
||||
onState: function (data) {
|
||||
this._newEngines = data.engines;
|
||||
this._setCurrentEngine(data.currentEngine);
|
||||
this._initWhenInitalStateReceived();
|
||||
},
|
||||
|
||||
onCurrentState: function (data) {
|
||||
if (this._initialStateReceived) {
|
||||
this._newEngines = data.engines;
|
||||
this._setCurrentEngine(data.currentEngine);
|
||||
}
|
||||
},
|
||||
|
||||
onCurrentEngine: function (engineName) {
|
||||
if (this._initialStateReceived) {
|
||||
this._nodes.panel.hidePopup();
|
||||
this._setCurrentEngine(engineName);
|
||||
}
|
||||
},
|
||||
|
||||
onFocusInput: function () {
|
||||
this._nodes.text.focus();
|
||||
},
|
||||
|
||||
_nodeIDSuffixes: [
|
||||
"form",
|
||||
"logo",
|
||||
"manage",
|
||||
"panel",
|
||||
"text",
|
||||
],
|
||||
|
||||
_nodes: {},
|
||||
|
||||
_initWhenInitalStateReceived: function () {
|
||||
this._nodes.form.addEventListener("submit", e => this.search(e));
|
||||
this._nodes.logo.addEventListener("click", e => this.showPanel());
|
||||
this._nodes.manage.addEventListener("click", e => this.manageEngines());
|
||||
this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel());
|
||||
this._initialStateReceived = true;
|
||||
this._initWhenInitalStateReceived = function () {};
|
||||
},
|
||||
|
||||
_send: function (type, data=null) {
|
||||
window.dispatchEvent(new CustomEvent("ContentSearchClient", {
|
||||
detail: {
|
||||
type: type,
|
||||
data: data,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
_setUpPanel: function () {
|
||||
// The new search UI only contains the "manage" engine entry in the panel
|
||||
if (this.useNewUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the panel if necessary.
|
||||
if (this._newEngines) {
|
||||
this._buildPanel(this._newEngines);
|
||||
delete this._newEngines;
|
||||
}
|
||||
|
||||
// Set the selected states of the engines.
|
||||
let panel = this._nodes.panel;
|
||||
for (let box of panel.childNodes) {
|
||||
if (box.getAttribute("engine") == this.currentEngineName) {
|
||||
box.setAttribute("selected", "true");
|
||||
}
|
||||
else {
|
||||
box.removeAttribute("selected");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_buildPanel: function (engines) {
|
||||
let panel = this._nodes.panel;
|
||||
|
||||
// Empty the panel except for the Manage Engines row.
|
||||
let i = 0;
|
||||
while (i < panel.childNodes.length) {
|
||||
let node = panel.childNodes[i];
|
||||
if (node != this._nodes.manage) {
|
||||
panel.removeChild(node);
|
||||
}
|
||||
else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add all the engines.
|
||||
for (let engine of engines) {
|
||||
panel.insertBefore(this._makePanelEngine(panel, engine),
|
||||
this._nodes.manage);
|
||||
}
|
||||
},
|
||||
|
||||
// Converts favicon array buffer into data URI of the right size and dpi.
|
||||
_getFaviconURIFromBuffer: function (buffer) {
|
||||
let blob = new Blob([buffer]);
|
||||
let dpiSize = Math.round(16 * window.devicePixelRatio);
|
||||
let sizeStr = dpiSize + "," + dpiSize;
|
||||
return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
|
||||
},
|
||||
|
||||
_makePanelEngine: function (panel, engine) {
|
||||
let box = document.createElementNS(XUL_NAMESPACE, "hbox");
|
||||
box.className = "newtab-search-panel-engine";
|
||||
box.setAttribute("engine", engine.name);
|
||||
|
||||
box.addEventListener("click", () => {
|
||||
this._send("SetCurrentEngine", engine.name);
|
||||
panel.hidePopup();
|
||||
this._nodes.text.focus();
|
||||
});
|
||||
|
||||
let image = document.createElementNS(XUL_NAMESPACE, "image");
|
||||
if (engine.iconBuffer) {
|
||||
let uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
|
||||
image.setAttribute("src", uri);
|
||||
}
|
||||
box.appendChild(image);
|
||||
|
||||
let label = document.createElementNS(XUL_NAMESPACE, "label");
|
||||
label.setAttribute("value", engine.name);
|
||||
box.appendChild(label);
|
||||
|
||||
return box;
|
||||
},
|
||||
|
||||
_setCurrentEngine: function (engine) {
|
||||
this.currentEngineName = engine.name;
|
||||
|
||||
if (!this.useNewUI) {
|
||||
let type = "";
|
||||
let uri;
|
||||
let logoBuf = window.devicePixelRatio >= 2 ?
|
||||
engine.logo2xBuffer || engine.logoBuffer :
|
||||
engine.logoBuffer || engine.logo2xBuffer;
|
||||
if (logoBuf) {
|
||||
uri = URL.createObjectURL(new Blob([logoBuf]));
|
||||
type = "logo";
|
||||
}
|
||||
else if (engine.iconBuffer) {
|
||||
uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
|
||||
type = "favicon";
|
||||
}
|
||||
this._nodes.logo.setAttribute("type", type);
|
||||
|
||||
if (uri) {
|
||||
this._nodes.logo.style.backgroundImage = "url(" + uri + ")";
|
||||
}
|
||||
else {
|
||||
this._nodes.logo.style.backgroundImage = "";
|
||||
}
|
||||
this._nodes.text.placeholder = engine.placeholder;
|
||||
}
|
||||
|
||||
// Set up the suggestion controller.
|
||||
if (!this._suggestionController) {
|
||||
let parent = document.getElementById("newtab-scrollbox");
|
||||
this._suggestionController =
|
||||
new SearchSuggestionUIController(this._nodes.text, parent,
|
||||
event => this.search(event));
|
||||
}
|
||||
this._suggestionController.engineName = engine.name;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
.searchSuggestionTable {
|
||||
background-color: hsla(0,0%,100%,.99);
|
||||
border: 1px solid;
|
||||
border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
|
||||
border-spacing: 0;
|
||||
border-top: 0;
|
||||
box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
|
||||
0 0 2px hsla(210,65%,9%,.1) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
text-align: start;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.searchSuggestionRow {
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
max-width: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.searchSuggestionRow.formHistory + .searchSuggestionRow.remote > td {
|
||||
border-top: 1px solid GrayText;
|
||||
}
|
||||
|
||||
.searchSuggestionRow.selected {
|
||||
background-color: hsl(210,100%,40%);
|
||||
color: hsl(0,0%,100%);
|
||||
}
|
||||
|
||||
.searchSuggestionEntry {
|
||||
margin: 0;
|
||||
max-width: inherit;
|
||||
overflow: hidden;
|
||||
padding: 6px 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -0,0 +1,400 @@
|
|||
/* 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";
|
||||
|
||||
this.SearchSuggestionUIController = (function () {
|
||||
|
||||
const MAX_DISPLAYED_SUGGESTIONS = 6;
|
||||
const SUGGESTION_ID_PREFIX = "searchSuggestion";
|
||||
const CSS_URI = "chrome://browser/content/searchSuggestionUI.css";
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* Creates a new object that manages search suggestions and their UI for a text
|
||||
* box.
|
||||
*
|
||||
* The UI consists of an html:table that's inserted into the DOM after the given
|
||||
* text box and styled so that it appears as a dropdown below the text box.
|
||||
*
|
||||
* @param inputElement
|
||||
* Search suggestions will be based on the text in this text box.
|
||||
* Assumed to be an html:input. xul:textbox is untested but might work.
|
||||
* @param tableParent
|
||||
* The suggestion table is appended as a child to this element. Since
|
||||
* the table is absolutely positioned and its top and left values are set
|
||||
* to be relative to the top and left of the page, either the parent and
|
||||
* all its ancestors should not be positioned elements (i.e., their
|
||||
* positions should be "static"), or the parent's position should be the
|
||||
* top left of the page.
|
||||
* @param onClick
|
||||
* A function that's called when a search suggestion is clicked. Ideally
|
||||
* we could call submit() on inputElement's ancestor form, but that
|
||||
* doesn't trigger submit listeners. The function is passed one argument,
|
||||
* the click event.
|
||||
* @param idPrefix
|
||||
* The IDs of elements created by the object will be prefixed with this
|
||||
* string.
|
||||
*/
|
||||
function SearchSuggestionUIController(inputElement, tableParent, onClick=null,
|
||||
idPrefix="") {
|
||||
this.input = inputElement;
|
||||
this.onClick = onClick;
|
||||
this._idPrefix = idPrefix;
|
||||
|
||||
let tableID = idPrefix + "searchSuggestionTable";
|
||||
this.input.autocomplete = "off";
|
||||
this.input.setAttribute("aria-autocomplete", "true");
|
||||
this.input.setAttribute("aria-controls", tableID);
|
||||
tableParent.appendChild(this._makeTable(tableID));
|
||||
|
||||
this.input.addEventListener("keypress", this);
|
||||
this.input.addEventListener("input", this);
|
||||
this.input.addEventListener("focus", this);
|
||||
this.input.addEventListener("blur", this);
|
||||
window.addEventListener("ContentSearchService", this);
|
||||
|
||||
this._stickyInputValue = "";
|
||||
this._hideSuggestions();
|
||||
|
||||
this._ignoreInputEvent = false;
|
||||
}
|
||||
|
||||
SearchSuggestionUIController.prototype = {
|
||||
|
||||
// The timeout (ms) of the remote suggestions. Corresponds to
|
||||
// SearchSuggestionController.remoteTimeout. Uses
|
||||
// SearchSuggestionController's default timeout if falsey.
|
||||
remoteTimeout: undefined,
|
||||
|
||||
get engineName() {
|
||||
return this._engineName;
|
||||
},
|
||||
|
||||
set engineName(val) {
|
||||
this._engineName = val;
|
||||
if (val && document.activeElement == this.input) {
|
||||
this._speculativeConnect();
|
||||
}
|
||||
},
|
||||
|
||||
get selectedIndex() {
|
||||
for (let i = 0; i < this._table.children.length; i++) {
|
||||
let row = this._table.children[i];
|
||||
if (row.classList.contains("selected")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
set selectedIndex(idx) {
|
||||
// Update the table's rows, and the input when there is a selection.
|
||||
this._table.removeAttribute("aria-activedescendant");
|
||||
for (let i = 0; i < this._table.children.length; i++) {
|
||||
let row = this._table.children[i];
|
||||
if (i == idx) {
|
||||
row.classList.add("selected");
|
||||
row.firstChild.setAttribute("aria-selected", "true");
|
||||
this._table.setAttribute("aria-activedescendant", row.firstChild.id);
|
||||
}
|
||||
else {
|
||||
row.classList.remove("selected");
|
||||
row.firstChild.setAttribute("aria-selected", "false");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get numSuggestions() {
|
||||
return this._table.children.length;
|
||||
},
|
||||
|
||||
selectAndUpdateInput: function (idx) {
|
||||
this.selectedIndex = idx;
|
||||
this.input.value = idx >= 0 ? this.suggestionAtIndex(idx) :
|
||||
this._stickyInputValue;
|
||||
},
|
||||
|
||||
suggestionAtIndex: function (idx) {
|
||||
let row = this._table.children[idx];
|
||||
return row ? row.textContent : null;
|
||||
},
|
||||
|
||||
deleteSuggestionAtIndex: function (idx) {
|
||||
// Only form history suggestions can be deleted.
|
||||
if (this.isFormHistorySuggestionAtIndex(idx)) {
|
||||
let suggestionStr = this.suggestionAtIndex(idx);
|
||||
this._sendMsg("RemoveFormHistoryEntry", suggestionStr);
|
||||
this._table.children[idx].remove();
|
||||
this.selectAndUpdateInput(-1);
|
||||
}
|
||||
},
|
||||
|
||||
isFormHistorySuggestionAtIndex: function (idx) {
|
||||
let row = this._table.children[idx];
|
||||
return row && row.classList.contains("formHistory");
|
||||
},
|
||||
|
||||
addInputValueToFormHistory: function () {
|
||||
this._sendMsg("AddFormHistoryEntry", this.input.value);
|
||||
},
|
||||
|
||||
handleEvent: function (event) {
|
||||
this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
|
||||
},
|
||||
|
||||
_onInput: function (event) {
|
||||
if (this._ignoreInputEvent) {
|
||||
return;
|
||||
}
|
||||
if (this.input.value) {
|
||||
this._getSuggestions();
|
||||
}
|
||||
else {
|
||||
this._stickyInputValue = "";
|
||||
this._hideSuggestions();
|
||||
}
|
||||
this.selectedIndex = -1;
|
||||
},
|
||||
|
||||
_onKeypress: function (event) {
|
||||
let selectedIndexDelta = 0;
|
||||
switch (event.keyCode) {
|
||||
case event.DOM_VK_UP:
|
||||
if (this.numSuggestions) {
|
||||
selectedIndexDelta = -1;
|
||||
}
|
||||
break;
|
||||
case event.DOM_VK_DOWN:
|
||||
if (this.numSuggestions) {
|
||||
selectedIndexDelta = 1;
|
||||
}
|
||||
else {
|
||||
this._getSuggestions();
|
||||
}
|
||||
break;
|
||||
case event.DOM_VK_RIGHT:
|
||||
// Allow normal caret movement until the caret is at the end of the input.
|
||||
if (this.input.selectionStart != this.input.selectionEnd ||
|
||||
this.input.selectionEnd != this.input.value.length) {
|
||||
return;
|
||||
}
|
||||
// else, fall through
|
||||
case event.DOM_VK_RETURN:
|
||||
if (this.selectedIndex >= 0) {
|
||||
this.input.value = this.suggestionAtIndex(this.selectedIndex);
|
||||
this.input.setAttribute("selection-index", this.selectedIndex);
|
||||
this.input.setAttribute("selection-kind", "key");
|
||||
} else {
|
||||
// If we didn't select anything, make sure to remove the attributes
|
||||
// in case they were populated last time.
|
||||
this.input.removeAttribute("selection-index");
|
||||
this.input.removeAttribute("selection-kind");
|
||||
}
|
||||
this._stickyInputValue = this.input.value;
|
||||
this._hideSuggestions();
|
||||
break;
|
||||
case event.DOM_VK_DELETE:
|
||||
if (this.selectedIndex >= 0) {
|
||||
this.deleteSuggestionAtIndex(this.selectedIndex);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIndexDelta) {
|
||||
// Update the selection.
|
||||
let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
|
||||
if (newSelectedIndex < -1) {
|
||||
newSelectedIndex = this.numSuggestions - 1;
|
||||
}
|
||||
else if (this.numSuggestions <= newSelectedIndex) {
|
||||
newSelectedIndex = -1;
|
||||
}
|
||||
this.selectAndUpdateInput(newSelectedIndex);
|
||||
|
||||
// Prevent the input's caret from moving.
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
_onFocus: function () {
|
||||
this._speculativeConnect();
|
||||
},
|
||||
|
||||
_onBlur: function () {
|
||||
this._hideSuggestions();
|
||||
},
|
||||
|
||||
_onMousemove: function (event) {
|
||||
this.selectedIndex = this._indexOfTableRowOrDescendent(event.target);
|
||||
},
|
||||
|
||||
_onMousedown: function (event) {
|
||||
if (event.button == 2) {
|
||||
return;
|
||||
}
|
||||
let idx = this._indexOfTableRowOrDescendent(event.target);
|
||||
let suggestion = this.suggestionAtIndex(idx);
|
||||
this._stickyInputValue = suggestion;
|
||||
|
||||
// Setting value commits composition string forcibly. While IME commits
|
||||
// composition, this needs to ignore input event at committed composition
|
||||
// string which will be overwritten by the suggestion.
|
||||
this._ignoreInputEvent = true;
|
||||
this.input.value = suggestion;
|
||||
this._ignoreInputEvent = false;
|
||||
this.input.setAttribute("selection-index", idx);
|
||||
this.input.setAttribute("selection-kind", "mouse");
|
||||
this._hideSuggestions();
|
||||
if (this.onClick) {
|
||||
this.onClick.call(null, event);
|
||||
}
|
||||
},
|
||||
|
||||
_onContentSearchService: function (event) {
|
||||
let methodName = "_onMsg" + event.detail.type;
|
||||
if (methodName in this) {
|
||||
this[methodName](event.detail.data);
|
||||
}
|
||||
},
|
||||
|
||||
_onMsgSuggestions: function (suggestions) {
|
||||
// Ignore the suggestions if their search string or engine doesn't match
|
||||
// ours. Due to the async nature of message passing, this can easily happen
|
||||
// when the user types quickly.
|
||||
if (this._stickyInputValue != suggestions.searchString ||
|
||||
this.engineName != suggestions.engineName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty the table.
|
||||
while (this._table.firstElementChild) {
|
||||
this._table.firstElementChild.remove();
|
||||
}
|
||||
|
||||
// Position and size the table.
|
||||
let { left, bottom } = this.input.getBoundingClientRect();
|
||||
this._table.style.left = (left + window.scrollX) + "px";
|
||||
this._table.style.top = (bottom + window.scrollY) + "px";
|
||||
this._table.style.minWidth = this.input.offsetWidth + "px";
|
||||
this._table.style.maxWidth = (window.innerWidth - left - 40) + "px";
|
||||
|
||||
// Add the suggestions to the table.
|
||||
let searchWords =
|
||||
new Set(suggestions.searchString.trim().toLowerCase().split(/\s+/));
|
||||
for (let i = 0; i < MAX_DISPLAYED_SUGGESTIONS; i++) {
|
||||
let type, idx;
|
||||
if (i < suggestions.formHistory.length) {
|
||||
[type, idx] = ["formHistory", i];
|
||||
}
|
||||
else {
|
||||
let j = i - suggestions.formHistory.length;
|
||||
if (j < suggestions.remote.length) {
|
||||
[type, idx] = ["remote", j];
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._table.appendChild(this._makeTableRow(type, suggestions[type][idx],
|
||||
i, searchWords));
|
||||
}
|
||||
|
||||
this._table.hidden = false;
|
||||
this.input.setAttribute("aria-expanded", "true");
|
||||
},
|
||||
|
||||
_speculativeConnect: function () {
|
||||
if (this.engineName) {
|
||||
this._sendMsg("SpeculativeConnect", this.engineName);
|
||||
}
|
||||
},
|
||||
|
||||
_makeTableRow: function (type, suggestionStr, currentRow, searchWords) {
|
||||
let row = document.createElementNS(HTML_NS, "tr");
|
||||
row.dir = "auto";
|
||||
row.classList.add("searchSuggestionRow");
|
||||
row.classList.add(type);
|
||||
row.setAttribute("role", "presentation");
|
||||
row.addEventListener("mousemove", this);
|
||||
row.addEventListener("mousedown", this);
|
||||
|
||||
let entry = document.createElementNS(HTML_NS, "td");
|
||||
entry.classList.add("searchSuggestionEntry");
|
||||
entry.setAttribute("role", "option");
|
||||
entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow;
|
||||
entry.setAttribute("aria-selected", "false");
|
||||
|
||||
let suggestionWords = suggestionStr.trim().toLowerCase().split(/\s+/);
|
||||
for (let i = 0; i < suggestionWords.length; i++) {
|
||||
let word = suggestionWords[i];
|
||||
let wordSpan = document.createElementNS(HTML_NS, "span");
|
||||
if (searchWords.has(word)) {
|
||||
wordSpan.classList.add("typed");
|
||||
}
|
||||
wordSpan.textContent = word;
|
||||
entry.appendChild(wordSpan);
|
||||
if (i < suggestionWords.length - 1) {
|
||||
entry.appendChild(document.createTextNode(" "));
|
||||
}
|
||||
}
|
||||
|
||||
row.appendChild(entry);
|
||||
return row;
|
||||
},
|
||||
|
||||
_getSuggestions: function () {
|
||||
this._stickyInputValue = this.input.value;
|
||||
if (this.engineName) {
|
||||
this._sendMsg("GetSuggestions", {
|
||||
engineName: this.engineName,
|
||||
searchString: this.input.value,
|
||||
remoteTimeout: this.remoteTimeout,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_hideSuggestions: function () {
|
||||
this.input.setAttribute("aria-expanded", "false");
|
||||
this._table.hidden = true;
|
||||
while (this._table.firstElementChild) {
|
||||
this._table.firstElementChild.remove();
|
||||
}
|
||||
this.selectAndUpdateInput(-1);
|
||||
},
|
||||
|
||||
_indexOfTableRowOrDescendent: function (row) {
|
||||
while (row && row.localName != "tr") {
|
||||
row = row.parentNode;
|
||||
}
|
||||
if (!row) {
|
||||
throw new Error("Element is not a row");
|
||||
}
|
||||
return row.rowIndex;
|
||||
},
|
||||
|
||||
_makeTable: function (id) {
|
||||
this._table = document.createElementNS(HTML_NS, "table");
|
||||
this._table.id = id;
|
||||
this._table.hidden = true;
|
||||
this._table.classList.add("searchSuggestionTable");
|
||||
this._table.setAttribute("role", "listbox");
|
||||
return this._table;
|
||||
},
|
||||
|
||||
_sendMsg: function (type, data=null) {
|
||||
dispatchEvent(new CustomEvent("ContentSearchClient", {
|
||||
detail: {
|
||||
type: type,
|
||||
data: data,
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
return SearchSuggestionUIController;
|
||||
})();
|
|
@ -114,6 +114,12 @@ let AboutHomeListener = {
|
|||
case "AboutHomeLoad":
|
||||
this.onPageLoad();
|
||||
break;
|
||||
case "AboutHomeSearchEvent":
|
||||
this.onSearch(aEvent);
|
||||
break;
|
||||
case "AboutHomeSearchPanel":
|
||||
this.onOpenSearchPanel(aEvent);
|
||||
break;
|
||||
case "click":
|
||||
this.onClick(aEvent);
|
||||
break;
|
||||
|
@ -131,6 +137,9 @@ let AboutHomeListener = {
|
|||
case "AboutHome:Update":
|
||||
this.onUpdate(aMessage.data);
|
||||
break;
|
||||
case "AboutHome:FocusInput":
|
||||
this.onFocusInput();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -141,11 +150,13 @@ let AboutHomeListener = {
|
|||
|
||||
// Inject search engine and snippets URL.
|
||||
let docElt = doc.documentElement;
|
||||
// Set snippetsVersion last, which triggers to show the snippets when it's set.
|
||||
// set the following attributes BEFORE searchEngineName, which triggers to
|
||||
// show the snippets when it's set.
|
||||
docElt.setAttribute("snippetsURL", aData.snippetsURL);
|
||||
if (aData.showKnowYourRights)
|
||||
docElt.setAttribute("showKnowYourRights", "true");
|
||||
docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
|
||||
docElt.setAttribute("searchEngineName", aData.defaultEngineName);
|
||||
},
|
||||
|
||||
onPageLoad: function() {
|
||||
|
@ -156,6 +167,7 @@ let AboutHomeListener = {
|
|||
|
||||
doc.documentElement.setAttribute("hasBrowserHandlers", "true");
|
||||
addMessageListener("AboutHome:Update", this);
|
||||
addMessageListener("AboutHome:FocusInput", this);
|
||||
addEventListener("click", this, true);
|
||||
addEventListener("pagehide", this, true);
|
||||
|
||||
|
@ -164,6 +176,8 @@ let AboutHomeListener = {
|
|||
}
|
||||
|
||||
sendAsyncMessage("AboutHome:RequestUpdate");
|
||||
doc.addEventListener("AboutHomeSearchEvent", this, true, true);
|
||||
doc.addEventListener("AboutHomeSearchPanel", this, true, true);
|
||||
},
|
||||
|
||||
onClick: function(aEvent) {
|
||||
|
@ -214,6 +228,10 @@ let AboutHomeListener = {
|
|||
case "settings":
|
||||
sendAsyncMessage("AboutHome:Settings");
|
||||
break;
|
||||
|
||||
case "searchIcon":
|
||||
sendAsyncMessage("AboutHome:OpenSearchPanel", null, { anchor: originalTarget });
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -228,6 +246,21 @@ let AboutHomeListener = {
|
|||
aEvent.target.documentElement.removeAttribute("hasBrowserHandlers");
|
||||
}
|
||||
},
|
||||
|
||||
onSearch: function(aEvent) {
|
||||
sendAsyncMessage("AboutHome:Search", { searchData: aEvent.detail });
|
||||
},
|
||||
|
||||
onOpenSearchPanel: function(aEvent) {
|
||||
sendAsyncMessage("AboutHome:OpenSearchPanel");
|
||||
},
|
||||
|
||||
onFocusInput: function () {
|
||||
let searchInput = content.document.getElementById("searchText");
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
}
|
||||
},
|
||||
};
|
||||
AboutHomeListener.init(this);
|
||||
|
||||
|
|
|
@ -80,7 +80,6 @@ support-files =
|
|||
redirect_bug623155.sjs
|
||||
searchSuggestionEngine.sjs
|
||||
searchSuggestionEngine.xml
|
||||
searchSuggestionEngine2.xml
|
||||
subtst_contextmenu.html
|
||||
test-mixedcontent-securityerrors.html
|
||||
test_bug435035.html
|
||||
|
@ -370,10 +369,10 @@ skip-if = buildapp == 'mulet' || e10s # Bug 933103 - mochitest's EventUtils.synt
|
|||
skip-if = buildapp == 'mulet'
|
||||
[browser_save_video_frame.js]
|
||||
[browser_scope.js]
|
||||
[browser_contentSearchUI.js]
|
||||
[browser_searchSuggestionUI.js]
|
||||
support-files =
|
||||
contentSearchUI.html
|
||||
contentSearchUI.js
|
||||
searchSuggestionUI.html
|
||||
searchSuggestionUI.js
|
||||
[browser_selectpopup.js]
|
||||
run-if = e10s
|
||||
[browser_selectTabAtIndex.js]
|
||||
|
|
|
@ -101,15 +101,28 @@ let gTests = [
|
|||
// Make this actually work in healthreport by giving it an ID:
|
||||
engine.wrappedJSObject._identifier = 'org.mozilla.testsearchsuggestions';
|
||||
|
||||
let p = promiseContentSearchChange(engine.name);
|
||||
let promise = promiseBrowserAttributes(gBrowser.selectedTab);
|
||||
Services.search.currentEngine = engine;
|
||||
yield p;
|
||||
yield promise;
|
||||
|
||||
let numSearchesBefore = 0;
|
||||
let searchEventDeferred = Promise.defer();
|
||||
let doc = gBrowser.contentDocument;
|
||||
let engineName = gBrowser.contentWindow.wrappedJSObject.gContentSearchController.defaultEngine.name;
|
||||
let engineName = doc.documentElement.getAttribute("searchEngineName");
|
||||
is(engine.name, engineName, "Engine name in DOM should match engine we just added");
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
mm.loadFrameScript(TEST_CONTENT_HELPER, false);
|
||||
|
||||
mm.addMessageListener("AboutHomeTest:CheckRecordedSearch", function (msg) {
|
||||
let data = JSON.parse(msg.data);
|
||||
is(data.engineName, engineName, "Detail is search engine name");
|
||||
|
||||
getNumberOfSearches(engineName).then(num => {
|
||||
is(num, numSearchesBefore + 1, "One more search recorded.");
|
||||
searchEventDeferred.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Get the current number of recorded searches.
|
||||
let searchStr = "a search";
|
||||
|
@ -124,12 +137,7 @@ let gTests = [
|
|||
let expectedURL = Services.search.currentEngine.
|
||||
getSubmission(searchStr, null, "homepage").
|
||||
uri.spec;
|
||||
let loadPromise = waitForDocLoadAndStopIt(expectedURL).then(() => {
|
||||
getNumberOfSearches(engineName).then(num => {
|
||||
is(num, numSearchesBefore + 1, "One more search recorded.");
|
||||
searchEventDeferred.resolve();
|
||||
});
|
||||
});
|
||||
let loadPromise = waitForDocLoadAndStopIt(expectedURL);
|
||||
|
||||
try {
|
||||
yield Promise.all([searchEventDeferred.promise, loadPromise]);
|
||||
|
@ -228,11 +236,11 @@ let gTests = [
|
|||
{
|
||||
desc: "Check POST search engine support",
|
||||
setup: function() {},
|
||||
run: function* ()
|
||||
run: function()
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let currEngine = Services.search.defaultEngine;
|
||||
let searchObserver = Task.async(function* search_observer(aSubject, aTopic, aData) {
|
||||
let searchObserver = function search_observer(aSubject, aTopic, aData) {
|
||||
let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
|
||||
info("Observer: " + aData + " for " + engine.name);
|
||||
|
||||
|
@ -247,15 +255,24 @@ let gTests = [
|
|||
let document = gBrowser.selectedBrowser.contentDocument;
|
||||
let searchText = document.getElementById("searchText");
|
||||
|
||||
let p = promiseContentSearchChange(engine.name);
|
||||
Services.search.defaultEngine = engine;
|
||||
yield p;
|
||||
// We're about to change the search engine. Once the change has
|
||||
// propagated to the about:home content, we want to perform a search.
|
||||
let mutationObserver = new MutationObserver(function (mutations) {
|
||||
for (let mutation of mutations) {
|
||||
if (mutation.attributeName == "searchEngineName") {
|
||||
searchText.value = needle;
|
||||
searchText.focus();
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
}
|
||||
}
|
||||
});
|
||||
mutationObserver.observe(document.documentElement, { attributes: true });
|
||||
|
||||
searchText.value = needle;
|
||||
searchText.focus();
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
// Change the search engine, triggering the observer above.
|
||||
Services.search.defaultEngine = engine;
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
mutationObserver.disconnect();
|
||||
Services.search.removeEngine(engine);
|
||||
Services.search.defaultEngine = currEngine;
|
||||
});
|
||||
|
@ -269,7 +286,7 @@ let gTests = [
|
|||
"Search text should arrive correctly");
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false);
|
||||
registerCleanupFunction(function () {
|
||||
Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
|
||||
|
@ -318,7 +335,8 @@ let gTests = [
|
|||
},
|
||||
|
||||
{
|
||||
// See browser_contentSearchUI.js for comprehensive content search UI tests.
|
||||
// See browser_searchSuggestionUI.js for comprehensive content search
|
||||
// suggestion UI tests.
|
||||
desc: "Search suggestion smoke test",
|
||||
setup: function() {},
|
||||
run: function()
|
||||
|
@ -326,12 +344,12 @@ let gTests = [
|
|||
return Task.spawn(function* () {
|
||||
// Add a test engine that provides suggestions and switch to it.
|
||||
let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
|
||||
let p = promiseContentSearchChange(engine.name);
|
||||
let promise = promiseBrowserAttributes(gBrowser.selectedTab);
|
||||
Services.search.currentEngine = engine;
|
||||
yield p;
|
||||
yield promise;
|
||||
|
||||
// Avoid intermittent failures.
|
||||
gBrowser.contentWindow.wrappedJSObject.gContentSearchController.remoteTimeout = 5000;
|
||||
gBrowser.contentWindow.wrappedJSObject.gSearchSuggestionController.remoteTimeout = 5000;
|
||||
|
||||
// Type an X in the search input.
|
||||
let input = gBrowser.contentDocument.getElementById("searchText");
|
||||
|
@ -383,11 +401,9 @@ let gTests = [
|
|||
caret: { start: 1, length: 0 }
|
||||
}, gBrowser.contentWindow);
|
||||
|
||||
let searchController =
|
||||
gBrowser.contentWindow.wrappedJSObject.gContentSearchController;
|
||||
|
||||
// Wait for the search suggestions to become visible.
|
||||
let table = searchController._suggestionsList;
|
||||
let table =
|
||||
gBrowser.contentDocument.getElementById("searchSuggestionTable");
|
||||
let deferred = Promise.defer();
|
||||
let observer = new MutationObserver(() => {
|
||||
if (input.getAttribute("aria-expanded") == "true") {
|
||||
|
@ -408,14 +424,9 @@ let gTests = [
|
|||
uri.spec;
|
||||
let loadPromise = waitForDocLoadAndStopIt(expectedURL);
|
||||
let row = table.children[1];
|
||||
// ContentSearchUIController looks at the current selectedIndex when
|
||||
// performing a search. Synthesizing the mouse event on the suggestion
|
||||
// doesn't actually mouseover the suggestion and trigger it to be flagged
|
||||
// as selected, so we manually select it first.
|
||||
searchController.selectedIndex = 1;
|
||||
EventUtils.synthesizeMouseAtCenter(row, {button: 0}, gBrowser.contentWindow);
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" }, row, gBrowser.contentWindow);
|
||||
yield loadPromise;
|
||||
ok(input.value == "x", "Input value did not change");
|
||||
ok(input.value == "xbar", "Suggestion is selected");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -466,6 +477,26 @@ let gTests = [
|
|||
is(gBrowser.currentURI.spec, "about:accounts?entrypoint=abouthome",
|
||||
"Entry point should be `abouthome`.");
|
||||
})
|
||||
},
|
||||
{
|
||||
desc: "Clicking the icon should open the popup",
|
||||
setup: function () {},
|
||||
run: Task.async(function* () {
|
||||
let doc = gBrowser.selectedBrowser.contentDocument;
|
||||
let searchIcon = doc.getElementById("searchIcon");
|
||||
let panel = window.document.getElementById("abouthome-search-panel");
|
||||
|
||||
info("Waiting for popup to open");
|
||||
EventUtils.synthesizeMouseAtCenter(searchIcon, {}, gBrowser.selectedBrowser.contentWindow);
|
||||
yield promiseWaitForEvent(panel, "popupshown");
|
||||
info("Saw popup open");
|
||||
|
||||
let promise = promisePrefsOpen();
|
||||
let item = window.document.getElementById("abouthome-search-panel-manage");
|
||||
EventUtils.synthesizeMouseAtCenter(item, {});
|
||||
|
||||
yield promise;
|
||||
})
|
||||
}
|
||||
|
||||
];
|
||||
|
@ -545,6 +576,38 @@ function promiseSetupSnippetsMap(aTab, aSetupFn)
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the attributes being set by browser.js.
|
||||
*
|
||||
* @param aTab
|
||||
* The tab containing about:home.
|
||||
* @return {Promise} resolved when the attributes are ready.
|
||||
*/
|
||||
function promiseBrowserAttributes(aTab)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let docElt = aTab.linkedBrowser.contentDocument.documentElement;
|
||||
let observer = new MutationObserver(function (mutations) {
|
||||
for (let mutation of mutations) {
|
||||
info("Got attribute mutation: " + mutation.attributeName +
|
||||
" from " + mutation.oldValue);
|
||||
// Now we just have to wait for the last attribute.
|
||||
if (mutation.attributeName == "searchEngineName") {
|
||||
info("Remove attributes observer");
|
||||
observer.disconnect();
|
||||
// Must be sure to continue after the page mutation observer.
|
||||
executeSoon(function() deferred.resolve());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
info("Add attributes observer");
|
||||
observer.observe(docElt, { attributes: true });
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of about:home searches recorded for the current day.
|
||||
*
|
||||
|
@ -648,18 +711,6 @@ let promisePrefsOpen = Task.async(function*() {
|
|||
}
|
||||
});
|
||||
|
||||
function promiseContentSearchChange(newEngineName) {
|
||||
return new Promise(resolve => {
|
||||
content.addEventListener("ContentSearchService", function listener(aEvent) {
|
||||
if (aEvent.detail.type == "CurrentState" &&
|
||||
gBrowser.contentWindow.wrappedJSObject.gContentSearchController.defaultEngine.name == newEngineName) {
|
||||
content.removeEventListener("ContentSearchService", listener);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promiseNewEngine(basename) {
|
||||
info("Waiting for engine to be added: " + basename);
|
||||
let addDeferred = Promise.defer();
|
||||
|
|
|
@ -1,529 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const TEST_PAGE_BASENAME = "contentSearchUI.html";
|
||||
const TEST_CONTENT_SCRIPT_BASENAME = "contentSearchUI.js";
|
||||
const TEST_ENGINE_PREFIX = "browser_searchSuggestionEngine";
|
||||
const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
|
||||
const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
|
||||
|
||||
const TEST_MSG = "ContentSearchUIControllerTest";
|
||||
|
||||
add_task(function* emptyInput() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_BACK_SPACE");
|
||||
checkState(state, "", [], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* blur() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("blur");
|
||||
checkState(state, "x", [], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* upDownKeys() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Cycle down the suggestions starting from no selection.
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbar", ["xfoo", "xbar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "x", ["xfoo", "xbar"], 2);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "x", ["xfoo", "xbar"], 3);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Cycle up starting from no selection.
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "x", ["xfoo", "xbar"], 3);
|
||||
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "x", ["xfoo", "xbar"], 2);
|
||||
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "xbar", ["xfoo", "xbar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* rightLeftKeys() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_LEFT");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_LEFT");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_RIGHT");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_RIGHT");
|
||||
checkState(state, "x", [], -1);
|
||||
|
||||
state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
|
||||
|
||||
// This should make the xfoo suggestion sticky. To make sure it sticks,
|
||||
// trigger suggestions again and cycle through them by pressing Down until
|
||||
// nothing is selected again.
|
||||
state = yield msg("key", "VK_RIGHT");
|
||||
checkState(state, "xfoo", [], 0);
|
||||
|
||||
state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
|
||||
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 2);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* mouse() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
state = yield msg("mousemove", i);
|
||||
checkState(state, "x", ["xfoo", "xbar"], i);
|
||||
}
|
||||
|
||||
state = yield msg("mousemove", -1);
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* formHistory() {
|
||||
yield setUp();
|
||||
|
||||
// Type an X and add it to form history.
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
// Wait for Satchel to say it's been added to form history.
|
||||
let deferred = Promise.defer();
|
||||
Services.obs.addObserver(function onAdd(subj, topic, data) {
|
||||
if (data == "formhistory-add") {
|
||||
Services.obs.removeObserver(onAdd, "satchel-storage-changed");
|
||||
executeSoon(() => deferred.resolve());
|
||||
}
|
||||
}, "satchel-storage-changed", false);
|
||||
yield Promise.all([msg("addInputValueToFormHistory"), deferred.promise]);
|
||||
|
||||
// Reset the input.
|
||||
state = yield msg("reset");
|
||||
checkState(state, "", [], -1);
|
||||
|
||||
// Type an X again. The form history entry should appear.
|
||||
state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
|
||||
-1);
|
||||
|
||||
// Select the form history entry and delete it.
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
|
||||
0);
|
||||
|
||||
// Wait for Satchel.
|
||||
deferred = Promise.defer();
|
||||
Services.obs.addObserver(function onRemove(subj, topic, data) {
|
||||
if (data == "formhistory-remove") {
|
||||
Services.obs.removeObserver(onRemove, "satchel-storage-changed");
|
||||
executeSoon(() => deferred.resolve());
|
||||
}
|
||||
}, "satchel-storage-changed", false);
|
||||
|
||||
state = yield msg("key", "VK_DELETE");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield deferred.promise;
|
||||
|
||||
// Reset the input.
|
||||
state = yield msg("reset");
|
||||
checkState(state, "", [], -1);
|
||||
|
||||
// Type an X again. The form history entry should still be gone.
|
||||
state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* search() {
|
||||
yield setUp();
|
||||
|
||||
let modifiers = {};
|
||||
["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(k => modifiers[k] = true);
|
||||
|
||||
// Test typing a query and pressing enter.
|
||||
let p = msg("waitForSearch");
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
|
||||
let mesg = yield p;
|
||||
let eventData = {
|
||||
engineName: TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME,
|
||||
searchString: "x",
|
||||
healthReportKey: "test",
|
||||
searchPurpose: "test",
|
||||
originalEvent: modifiers,
|
||||
};
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Test typing a query, then selecting a suggestion and pressing enter.
|
||||
p = msg("waitForSearch");
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
yield msg("key", "VK_DOWN");
|
||||
yield msg("key", "VK_DOWN");
|
||||
yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
eventData.searchString = "xfoo";
|
||||
eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
|
||||
eventData.selection = {
|
||||
index: 1,
|
||||
kind: "key",
|
||||
}
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Test typing a query, then selecting a one-off button and pressing enter.
|
||||
p = msg("waitForSearch");
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
yield msg("key", "VK_UP");
|
||||
yield msg("key", "VK_UP");
|
||||
yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
delete eventData.selection;
|
||||
eventData.searchString = "x";
|
||||
eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Test typing a query and clicking the search engine header.
|
||||
p = msg("waitForSearch");
|
||||
modifiers.button = 0;
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
yield msg("mousemove", -1);
|
||||
yield msg("click", { eltIdx: -1, modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
eventData.originalEvent = modifiers;
|
||||
eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Test typing a query and then clicking a suggestion.
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
p = msg("waitForSearch");
|
||||
yield msg("mousemove", 1);
|
||||
yield msg("click", { eltIdx: 1, modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
eventData.searchString = "xfoo";
|
||||
eventData.selection = {
|
||||
index: 1,
|
||||
kind: "mouse",
|
||||
};
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Test typing a query and then clicking a one-off button.
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
p = msg("waitForSearch");
|
||||
yield msg("mousemove", 3);
|
||||
yield msg("click", { eltIdx: 3, modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
eventData.searchString = "x";
|
||||
eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
|
||||
delete eventData.selection;
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Test searching when using IME composition.
|
||||
let state = yield msg("startComposition", { data: "" });
|
||||
checkState(state, "", [], -1);
|
||||
state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" },
|
||||
{ str: "xfoo", type: "formHistory" }, "xbar"], -1);
|
||||
yield msg("commitComposition");
|
||||
delete modifiers.button;
|
||||
p = msg("waitForSearch");
|
||||
yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
eventData.originalEvent = modifiers;
|
||||
eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
state = yield msg("startComposition", { data: "" });
|
||||
checkState(state, "", [], -1);
|
||||
state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" },
|
||||
{ str: "xfoo", type: "formHistory" }, "xbar"], -1);
|
||||
|
||||
// Mouse over the first suggestion.
|
||||
state = yield msg("mousemove", 0);
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" },
|
||||
{ str: "xfoo", type: "formHistory" }, "xbar"], 0);
|
||||
|
||||
// Mouse over the second suggestion.
|
||||
state = yield msg("mousemove", 1);
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" },
|
||||
{ str: "xfoo", type: "formHistory" }, "xbar"], 1);
|
||||
|
||||
modifiers.button = 0;
|
||||
let currentTab = gBrowser.selectedTab;
|
||||
p = msg("waitForSearch");
|
||||
yield msg("click", { eltIdx: 1, modifiers: modifiers });
|
||||
mesg = yield p;
|
||||
eventData.searchString = "xfoo";
|
||||
eventData.originalEvent = modifiers;
|
||||
eventData.selection = {
|
||||
index: 1,
|
||||
kind: "mouse",
|
||||
};
|
||||
SimpleTest.isDeeply(eventData, mesg, "Search event data");
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
|
||||
// Remove form history entries.
|
||||
// Wait for Satchel.
|
||||
let deferred = Promise.defer();
|
||||
let historyCount = 2;
|
||||
Services.obs.addObserver(function onRemove(subj, topic, data) {
|
||||
if (data == "formhistory-remove") {
|
||||
if (--historyCount) {
|
||||
return;
|
||||
}
|
||||
Services.obs.removeObserver(onRemove, "satchel-storage-changed");
|
||||
executeSoon(() => deferred.resolve());
|
||||
}
|
||||
}, "satchel-storage-changed", false);
|
||||
|
||||
yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
yield msg("key", "VK_DOWN");
|
||||
yield msg("key", "VK_DOWN");
|
||||
yield msg("key", "VK_DELETE");
|
||||
yield msg("key", "VK_DOWN");
|
||||
yield msg("key", "VK_DELETE");
|
||||
yield deferred.promise;
|
||||
|
||||
yield msg("reset");
|
||||
state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield promiseTab();
|
||||
yield setUp();
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* settings() {
|
||||
yield setUp();
|
||||
yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
|
||||
yield msg("key", "VK_UP");
|
||||
let p = msg("waitForSearchSettings");
|
||||
yield msg("key", "VK_RETURN");
|
||||
yield p;
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
let gDidInitialSetUp = false;
|
||||
|
||||
function setUp(aNoEngine) {
|
||||
return Task.spawn(function* () {
|
||||
if (!gDidInitialSetUp) {
|
||||
Cu.import("resource:///modules/ContentSearch.jsm");
|
||||
let originalOnMessageSearch = ContentSearch._onMessageSearch;
|
||||
let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;
|
||||
ContentSearch._onMessageSearch = () => {};
|
||||
ContentSearch._onMessageManageEngines = () => {};
|
||||
registerCleanupFunction(() => {
|
||||
ContentSearch._onMessageSearch = originalOnMessageSearch;
|
||||
ContentSearch._onMessageManageEngines = originalOnMessageManageEngines;
|
||||
});
|
||||
yield setUpEngines();
|
||||
yield promiseTab();
|
||||
gDidInitialSetUp = true;
|
||||
}
|
||||
yield msg("focus");
|
||||
});
|
||||
}
|
||||
|
||||
function msg(type, data=null) {
|
||||
gMsgMan.sendAsyncMessage(TEST_MSG, {
|
||||
type: type,
|
||||
data: data,
|
||||
});
|
||||
let deferred = Promise.defer();
|
||||
gMsgMan.addMessageListener(TEST_MSG, function onMsg(msg) {
|
||||
if (msg.data.type != type) {
|
||||
return;
|
||||
}
|
||||
gMsgMan.removeMessageListener(TEST_MSG, onMsg);
|
||||
deferred.resolve(msg.data.data);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function checkState(actualState, expectedInputVal, expectedSuggestions,
|
||||
expectedSelectedIdx) {
|
||||
expectedSuggestions = expectedSuggestions.map(sugg => {
|
||||
return typeof(sugg) == "object" ? sugg : {
|
||||
str: sugg,
|
||||
type: "remote",
|
||||
};
|
||||
});
|
||||
|
||||
let expectedState = {
|
||||
selectedIndex: expectedSelectedIdx,
|
||||
numSuggestions: expectedSuggestions.length,
|
||||
suggestionAtIndex: expectedSuggestions.map(s => s.str),
|
||||
isFormHistorySuggestionAtIndex:
|
||||
expectedSuggestions.map(s => s.type == "formHistory"),
|
||||
|
||||
tableHidden: expectedSuggestions.length == 0,
|
||||
|
||||
inputValue: expectedInputVal,
|
||||
ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
|
||||
};
|
||||
|
||||
SimpleTest.isDeeply(actualState, expectedState, "State");
|
||||
}
|
||||
|
||||
var gMsgMan;
|
||||
|
||||
function promiseTab() {
|
||||
let deferred = Promise.defer();
|
||||
let tab = gBrowser.addTab();
|
||||
registerCleanupFunction(() => gBrowser.removeTab(tab));
|
||||
gBrowser.selectedTab = tab;
|
||||
let pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
|
||||
tab.linkedBrowser.addEventListener("load", function onLoad(event) {
|
||||
tab.linkedBrowser.removeEventListener("load", onLoad, true);
|
||||
gMsgMan = tab.linkedBrowser.messageManager;
|
||||
gMsgMan.sendAsyncMessage("ContentSearch", {
|
||||
type: "AddToWhitelist",
|
||||
data: [pageURL],
|
||||
});
|
||||
promiseMsg("ContentSearch", "AddToWhitelistAck", gMsgMan).then(() => {
|
||||
let jsURL = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
|
||||
gMsgMan.loadFrameScript(jsURL, false);
|
||||
deferred.resolve(msg("init"));
|
||||
});
|
||||
}, true, true);
|
||||
openUILinkIn(pageURL, "current");
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promiseMsg(name, type, msgMan) {
|
||||
let deferred = Promise.defer();
|
||||
info("Waiting for " + name + " message " + type + "...");
|
||||
msgMan.addMessageListener(name, function onMsg(msg) {
|
||||
info("Received " + name + " message " + msg.data.type + "\n");
|
||||
if (msg.data.type == type) {
|
||||
msgMan.removeMessageListener(name, onMsg);
|
||||
deferred.resolve(msg);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function setUpEngines() {
|
||||
return Task.spawn(function* () {
|
||||
info("Removing default search engines");
|
||||
let currentEngineName = Services.search.currentEngine.name;
|
||||
let currentEngines = Services.search.getVisibleEngines();
|
||||
info("Adding test search engines");
|
||||
let engine1 = yield promiseNewEngine(TEST_ENGINE_BASENAME);
|
||||
let engine2 = yield promiseNewEngine(TEST_ENGINE_2_BASENAME);
|
||||
Services.search.currentEngine = engine1;
|
||||
for (let engine of currentEngines) {
|
||||
Services.search.removeEngine(engine);
|
||||
}
|
||||
registerCleanupFunction(() => {
|
||||
Services.search.restoreDefaultEngines();
|
||||
Services.search.removeEngine(engine1);
|
||||
Services.search.removeEngine(engine2);
|
||||
Services.search.currentEngine = Services.search.getEngineByName(currentEngineName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promiseNewEngine(basename) {
|
||||
info("Waiting for engine to be added: " + basename);
|
||||
let addDeferred = Promise.defer();
|
||||
let url = getRootDirectory(gTestPath) + basename;
|
||||
Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
|
||||
onSuccess: function (engine) {
|
||||
info("Search engine added: " + basename);
|
||||
addDeferred.resolve(engine);
|
||||
},
|
||||
onError: function (errCode) {
|
||||
ok(false, "addEngine failed with error code " + errCode);
|
||||
addDeferred.reject();
|
||||
},
|
||||
});
|
||||
return addDeferred.promise;
|
||||
}
|
|
@ -0,0 +1,345 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const TEST_PAGE_BASENAME = "searchSuggestionUI.html";
|
||||
const TEST_CONTENT_SCRIPT_BASENAME = "searchSuggestionUI.js";
|
||||
const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
|
||||
|
||||
const TEST_MSG = "SearchSuggestionUIControllerTest";
|
||||
|
||||
add_task(function* emptyInput() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_BACK_SPACE");
|
||||
checkState(state, "", [], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* blur() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("blur");
|
||||
checkState(state, "x", [], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* arrowKeys() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Cycle down the suggestions starting from no selection.
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbar", ["xfoo", "xbar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Cycle up starting from no selection.
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "xbar", ["xfoo", "xbar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_UP");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
// The right arrow and return key function the same.
|
||||
function rightArrowOrReturn(keyName) {
|
||||
return function* rightArrowOrReturnTest() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoo", "xbar"], 0);
|
||||
|
||||
// This should make the xfoo suggestion sticky. To make sure it sticks,
|
||||
// trigger suggestions again and cycle through them by pressing Down until
|
||||
// nothing is selected again.
|
||||
state = yield msg("key", keyName);
|
||||
checkState(state, "xfoo", [], -1);
|
||||
|
||||
state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
|
||||
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
};
|
||||
}
|
||||
|
||||
add_task(rightArrowOrReturn("VK_RIGHT"));
|
||||
add_task(rightArrowOrReturn("VK_RETURN"));
|
||||
|
||||
add_task(function* mouse() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Mouse over the first suggestion.
|
||||
state = yield msg("mousemove", 0);
|
||||
checkState(state, "x", ["xfoo", "xbar"], 0);
|
||||
|
||||
// Mouse over the second suggestion.
|
||||
state = yield msg("mousemove", 1);
|
||||
checkState(state, "x", ["xfoo", "xbar"], 1);
|
||||
|
||||
// Click the second suggestion. This should make it sticky. To make sure it
|
||||
// sticks, trigger suggestions again and cycle through them by pressing Down
|
||||
// until nothing is selected again.
|
||||
state = yield msg("mousedown", 1);
|
||||
checkState(state, "xbar", [], -1);
|
||||
|
||||
state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
|
||||
checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* formHistory() {
|
||||
yield setUp();
|
||||
|
||||
// Type an X and add it to form history.
|
||||
let state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
yield msg("addInputValueToFormHistory");
|
||||
|
||||
// Wait for Satchel to say it's been added to form history.
|
||||
let deferred = Promise.defer();
|
||||
Services.obs.addObserver(function onAdd(subj, topic, data) {
|
||||
if (data == "formhistory-add") {
|
||||
Services.obs.removeObserver(onAdd, "satchel-storage-changed");
|
||||
executeSoon(() => deferred.resolve());
|
||||
}
|
||||
}, "satchel-storage-changed", false);
|
||||
yield deferred.promise;
|
||||
|
||||
// Reset the input.
|
||||
state = yield msg("reset");
|
||||
checkState(state, "", [], -1);
|
||||
|
||||
// Type an X again. The form history entry should appear.
|
||||
state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
|
||||
-1);
|
||||
|
||||
// Select the form history entry and delete it.
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
|
||||
0);
|
||||
|
||||
state = yield msg("key", "VK_DELETE");
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Wait for Satchel.
|
||||
deferred = Promise.defer();
|
||||
Services.obs.addObserver(function onRemove(subj, topic, data) {
|
||||
if (data == "formhistory-remove") {
|
||||
Services.obs.removeObserver(onRemove, "satchel-storage-changed");
|
||||
executeSoon(() => deferred.resolve());
|
||||
}
|
||||
}, "satchel-storage-changed", false);
|
||||
yield deferred.promise;
|
||||
|
||||
// Reset the input.
|
||||
state = yield msg("reset");
|
||||
checkState(state, "", [], -1);
|
||||
|
||||
// Type an X again. The form history entry should still be gone.
|
||||
state = yield msg("key", { key: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
add_task(function* composition() {
|
||||
yield setUp();
|
||||
|
||||
let state = yield msg("startComposition", { data: "" });
|
||||
checkState(state, "", [], -1);
|
||||
state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
|
||||
checkState(state, "x", ["xfoo", "xbar"], -1);
|
||||
|
||||
// Mouse over the first suggestion.
|
||||
state = yield msg("mousemove", 0);
|
||||
checkState(state, "x", ["xfoo", "xbar"], 0);
|
||||
|
||||
// Mouse over the second suggestion.
|
||||
state = yield msg("mousemove", 1);
|
||||
checkState(state, "x", ["xfoo", "xbar"], 1);
|
||||
|
||||
// Click the second suggestion. This should make it sticky. To make sure it
|
||||
// sticks, trigger suggestions again and cycle through them by pressing Down
|
||||
// until nothing is selected again.
|
||||
state = yield msg("mousedown", 1);
|
||||
|
||||
checkState(state, "xbar", [], -1);
|
||||
|
||||
state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
|
||||
checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);
|
||||
|
||||
state = yield msg("key", "VK_DOWN");
|
||||
checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
|
||||
|
||||
yield msg("reset");
|
||||
});
|
||||
|
||||
|
||||
let gDidInitialSetUp = false;
|
||||
|
||||
function setUp() {
|
||||
return Task.spawn(function* () {
|
||||
if (!gDidInitialSetUp) {
|
||||
yield promiseNewEngine(TEST_ENGINE_BASENAME);
|
||||
yield promiseTab();
|
||||
gDidInitialSetUp = true;
|
||||
}
|
||||
yield msg("focus");
|
||||
});
|
||||
}
|
||||
|
||||
function msg(type, data=null) {
|
||||
gMsgMan.sendAsyncMessage(TEST_MSG, {
|
||||
type: type,
|
||||
data: data,
|
||||
});
|
||||
let deferred = Promise.defer();
|
||||
gMsgMan.addMessageListener(TEST_MSG, function onMsg(msg) {
|
||||
gMsgMan.removeMessageListener(TEST_MSG, onMsg);
|
||||
deferred.resolve(msg.data);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function checkState(actualState, expectedInputVal, expectedSuggestions,
|
||||
expectedSelectedIdx) {
|
||||
expectedSuggestions = expectedSuggestions.map(sugg => {
|
||||
return typeof(sugg) == "object" ? sugg : {
|
||||
str: sugg,
|
||||
type: "remote",
|
||||
};
|
||||
});
|
||||
|
||||
let expectedState = {
|
||||
selectedIndex: expectedSelectedIdx,
|
||||
numSuggestions: expectedSuggestions.length,
|
||||
suggestionAtIndex: expectedSuggestions.map(s => s.str),
|
||||
isFormHistorySuggestionAtIndex:
|
||||
expectedSuggestions.map(s => s.type == "formHistory"),
|
||||
|
||||
tableHidden: expectedSuggestions.length == 0,
|
||||
tableChildrenLength: expectedSuggestions.length,
|
||||
tableChildren: expectedSuggestions.map((s, i) => {
|
||||
let expectedClasses = new Set([s.type]);
|
||||
if (i == expectedSelectedIdx) {
|
||||
expectedClasses.add("selected");
|
||||
}
|
||||
return {
|
||||
textContent: s.str,
|
||||
classes: expectedClasses,
|
||||
};
|
||||
}),
|
||||
|
||||
inputValue: expectedInputVal,
|
||||
ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
|
||||
};
|
||||
|
||||
SimpleTest.isDeeply(actualState, expectedState, "State");
|
||||
}
|
||||
|
||||
var gMsgMan;
|
||||
|
||||
function promiseTab() {
|
||||
let deferred = Promise.defer();
|
||||
let tab = gBrowser.addTab();
|
||||
registerCleanupFunction(() => gBrowser.removeTab(tab));
|
||||
gBrowser.selectedTab = tab;
|
||||
let pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
|
||||
tab.linkedBrowser.addEventListener("load", function onLoad(event) {
|
||||
tab.linkedBrowser.removeEventListener("load", onLoad, true);
|
||||
gMsgMan = tab.linkedBrowser.messageManager;
|
||||
gMsgMan.sendAsyncMessage("ContentSearch", {
|
||||
type: "AddToWhitelist",
|
||||
data: [pageURL],
|
||||
});
|
||||
promiseMsg("ContentSearch", "AddToWhitelistAck", gMsgMan).then(() => {
|
||||
let jsURL = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
|
||||
gMsgMan.loadFrameScript(jsURL, false);
|
||||
deferred.resolve();
|
||||
});
|
||||
}, true, true);
|
||||
openUILinkIn(pageURL, "current");
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promiseMsg(name, type, msgMan) {
|
||||
let deferred = Promise.defer();
|
||||
info("Waiting for " + name + " message " + type + "...");
|
||||
msgMan.addMessageListener(name, function onMsg(msg) {
|
||||
info("Received " + name + " message " + msg.data.type + "\n");
|
||||
if (msg.data.type == type) {
|
||||
msgMan.removeMessageListener(name, onMsg);
|
||||
deferred.resolve(msg);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promiseNewEngine(basename) {
|
||||
info("Waiting for engine to be added: " + basename);
|
||||
let addDeferred = Promise.defer();
|
||||
let url = getRootDirectory(gTestPath) + basename;
|
||||
Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
|
||||
onSuccess: function (engine) {
|
||||
info("Search engine added: " + basename);
|
||||
registerCleanupFunction(() => Services.search.removeEngine(engine));
|
||||
addDeferred.resolve(engine);
|
||||
},
|
||||
onError: function (errCode) {
|
||||
ok(false, "addEngine failed with error code " + errCode);
|
||||
addDeferred.reject();
|
||||
},
|
||||
});
|
||||
return addDeferred.promise;
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
|
||||
const TEST_MSG = "ContentSearchUIControllerTest";
|
||||
const ENGINE_NAME = "browser_searchSuggestionEngine searchSuggestionEngine.xml";
|
||||
var gController;
|
||||
|
||||
addMessageListener(TEST_MSG, msg => {
|
||||
messageHandlers[msg.data.type](msg.data.data);
|
||||
});
|
||||
|
||||
let messageHandlers = {
|
||||
|
||||
init: function() {
|
||||
Services.search.currentEngine = Services.search.getEngineByName(ENGINE_NAME);
|
||||
let input = content.document.querySelector("input");
|
||||
gController =
|
||||
new content.ContentSearchUIController(input, input.parentNode, "test", "test");
|
||||
content.addEventListener("ContentSearchService", function listener(aEvent) {
|
||||
if (aEvent.detail.type == "State" &&
|
||||
gController.defaultEngine.name == ENGINE_NAME) {
|
||||
content.removeEventListener("ContentSearchService", listener);
|
||||
ack("init");
|
||||
}
|
||||
});
|
||||
gController.remoteTimeout = 5000;
|
||||
},
|
||||
|
||||
key: function (arg) {
|
||||
let keyName = typeof(arg) == "string" ? arg : arg.key;
|
||||
content.synthesizeKey(keyName, arg.modifiers || {});
|
||||
let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
|
||||
wait(ack.bind(null, "key"));
|
||||
},
|
||||
|
||||
startComposition: function (arg) {
|
||||
content.synthesizeComposition({ type: "compositionstart", data: "" });
|
||||
ack("startComposition");
|
||||
},
|
||||
|
||||
changeComposition: function (arg) {
|
||||
let data = typeof(arg) == "string" ? arg : arg.data;
|
||||
content.synthesizeCompositionChange({
|
||||
composition: {
|
||||
string: data,
|
||||
clauses: [
|
||||
{ length: data.length, attr: content.COMPOSITION_ATTR_RAW_CLAUSE }
|
||||
]
|
||||
},
|
||||
caret: { start: data.length, length: 0 }
|
||||
});
|
||||
let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
|
||||
wait(ack.bind(null, "changeComposition"));
|
||||
},
|
||||
|
||||
commitComposition: function () {
|
||||
content.synthesizeComposition({ type: "compositioncommitasis" });
|
||||
ack("commitComposition");
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
gController.input.focus();
|
||||
ack("focus");
|
||||
},
|
||||
|
||||
blur: function () {
|
||||
gController.input.blur();
|
||||
ack("blur");
|
||||
},
|
||||
|
||||
waitForSearch: function () {
|
||||
waitForContentSearchEvent("Search", aData => ack("waitForSearch", aData));
|
||||
},
|
||||
|
||||
waitForSearchSettings: function () {
|
||||
waitForContentSearchEvent("ManageEngines",
|
||||
aData => ack("waitForSearchSettings", aData));
|
||||
},
|
||||
|
||||
mousemove: function (itemIndex) {
|
||||
let row;
|
||||
if (itemIndex == -1) {
|
||||
row = gController._table.firstChild;
|
||||
}
|
||||
else {
|
||||
let allElts = [...gController._suggestionsList.children,
|
||||
...gController._oneOffButtons,
|
||||
content.document.getElementById("contentSearchSettingsButton")];
|
||||
row = allElts[itemIndex];
|
||||
}
|
||||
let event = {
|
||||
type: "mousemove",
|
||||
clickcount: 0,
|
||||
}
|
||||
content.synthesizeMouseAtCenter(row, event);
|
||||
ack("mousemove");
|
||||
},
|
||||
|
||||
click: function (arg) {
|
||||
let eltIdx = typeof(arg) == "object" ? arg.eltIdx : arg;
|
||||
let row;
|
||||
if (eltIdx == -1) {
|
||||
row = gController._table.firstChild;
|
||||
}
|
||||
else {
|
||||
let allElts = [...gController._suggestionsList.children,
|
||||
...gController._oneOffButtons,
|
||||
content.document.getElementById("contentSearchSettingsButton")];
|
||||
row = allElts[eltIdx];
|
||||
}
|
||||
let event = arg.modifiers || {};
|
||||
// synthesizeMouseAtCenter defaults to sending a mousedown followed by a
|
||||
// mouseup if the event type is not specified.
|
||||
content.synthesizeMouseAtCenter(row, event);
|
||||
ack("click");
|
||||
},
|
||||
|
||||
addInputValueToFormHistory: function () {
|
||||
gController.addInputValueToFormHistory();
|
||||
ack("addInputValueToFormHistory");
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
// Reset both the input and suggestions by select all + delete.
|
||||
gController.input.focus();
|
||||
content.synthesizeKey("a", { accelKey: true });
|
||||
content.synthesizeKey("VK_DELETE", {});
|
||||
ack("reset");
|
||||
},
|
||||
};
|
||||
|
||||
function ack(aType, aData) {
|
||||
sendAsyncMessage(TEST_MSG, { type: aType, data: aData || currentState() });
|
||||
}
|
||||
|
||||
function waitForSuggestions(cb) {
|
||||
let observer = new content.MutationObserver(() => {
|
||||
if (gController.input.getAttribute("aria-expanded") == "true") {
|
||||
observer.disconnect();
|
||||
cb();
|
||||
}
|
||||
});
|
||||
observer.observe(gController.input, {
|
||||
attributes: true,
|
||||
attributeFilter: ["aria-expanded"],
|
||||
});
|
||||
}
|
||||
|
||||
function waitForContentSearchEvent(messageType, cb) {
|
||||
let mm = content.SpecialPowers.Cc["@mozilla.org/globalmessagemanager;1"].
|
||||
getService(content.SpecialPowers.Ci.nsIMessageListenerManager);
|
||||
mm.addMessageListener("ContentSearch", function listener(aMsg) {
|
||||
if (aMsg.data.type != messageType) {
|
||||
return;
|
||||
}
|
||||
mm.removeMessageListener("ContentSearch", listener);
|
||||
cb(aMsg.data.data);
|
||||
});
|
||||
}
|
||||
|
||||
function currentState() {
|
||||
let state = {
|
||||
selectedIndex: gController.selectedIndex,
|
||||
numSuggestions: gController._table.hidden ? 0 : gController.numSuggestions,
|
||||
suggestionAtIndex: [],
|
||||
isFormHistorySuggestionAtIndex: [],
|
||||
|
||||
tableHidden: gController._table.hidden,
|
||||
|
||||
inputValue: gController.input.value,
|
||||
ariaExpanded: gController.input.getAttribute("aria-expanded"),
|
||||
};
|
||||
|
||||
if (state.numSuggestions) {
|
||||
for (let i = 0; i < gController.numSuggestions; i++) {
|
||||
state.suggestionAtIndex.push(gController.suggestionAtIndex(i));
|
||||
state.isFormHistorySuggestionAtIndex.push(
|
||||
gController.isFormHistorySuggestionAtIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
})();
|
|
@ -5,5 +5,5 @@
|
|||
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
||||
<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
|
||||
<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/general/searchSuggestionEngine.sjs?{searchTerms}"/>
|
||||
<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine&terms={searchTerms}" rel="searchform"/>
|
||||
<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine" rel="searchform"/>
|
||||
</SearchPlugin>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
|
||||
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
||||
<ShortName>browser_searchSuggestionEngine searchSuggestionEngine2.xml</ShortName>
|
||||
<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/general/searchSuggestionEngine.sjs?{searchTerms}"/>
|
||||
<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine2&terms={searchTerms}" rel="searchform"/>
|
||||
</SearchPlugin>
|
|
@ -9,13 +9,12 @@
|
|||
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
|
||||
</script>
|
||||
<script type="application/javascript;version=1.8"
|
||||
src="chrome://browser/content/contentSearchUI.js">
|
||||
src="chrome://browser/content/searchSuggestionUI.js">
|
||||
</script>
|
||||
<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="container" style="position: relative;"><input type="text" value=""/></div>
|
||||
<input>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,158 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
|
||||
const TEST_MSG = "SearchSuggestionUIControllerTest";
|
||||
const ENGINE_NAME = "browser_searchSuggestionEngine searchSuggestionEngine.xml";
|
||||
|
||||
let input = content.document.querySelector("input");
|
||||
let gController =
|
||||
new content.SearchSuggestionUIController(input, input.parentNode);
|
||||
gController.engineName = ENGINE_NAME;
|
||||
gController.remoteTimeout = 5000;
|
||||
|
||||
addMessageListener(TEST_MSG, msg => {
|
||||
messageHandlers[msg.data.type](msg.data.data);
|
||||
});
|
||||
|
||||
let messageHandlers = {
|
||||
|
||||
key: function (arg) {
|
||||
let keyName = typeof(arg) == "string" ? arg : arg.key;
|
||||
content.synthesizeKey(keyName, {});
|
||||
let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
|
||||
wait(ack);
|
||||
},
|
||||
|
||||
startComposition: function (arg) {
|
||||
content.synthesizeComposition({ type: "compositionstart", data: "" });
|
||||
ack();
|
||||
},
|
||||
|
||||
changeComposition: function (arg) {
|
||||
let data = typeof(arg) == "string" ? arg : arg.data;
|
||||
content.synthesizeCompositionChange({
|
||||
composition: {
|
||||
string: data,
|
||||
clauses: [
|
||||
{ length: data.length, attr: content.COMPOSITION_ATTR_RAW_CLAUSE }
|
||||
]
|
||||
},
|
||||
caret: { start: data.length, length: 0 }
|
||||
});
|
||||
let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
|
||||
wait(ack);
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
gController.input.focus();
|
||||
ack();
|
||||
},
|
||||
|
||||
blur: function () {
|
||||
gController.input.blur();
|
||||
ack();
|
||||
},
|
||||
|
||||
mousemove: function (suggestionIdx) {
|
||||
// Copied from widget/tests/test_panel_mouse_coords.xul and
|
||||
// browser/base/content/test/newtab/head.js
|
||||
let row = gController._table.children[suggestionIdx];
|
||||
let rect = row.getBoundingClientRect();
|
||||
let left = content.mozInnerScreenX + rect.left;
|
||||
let x = left + rect.width / 2;
|
||||
let y = content.mozInnerScreenY + rect.top + rect.height / 2;
|
||||
|
||||
let utils = content.SpecialPowers.getDOMWindowUtils(content);
|
||||
let scale = utils.screenPixelsPerCSSPixel;
|
||||
|
||||
let widgetToolkit = content.SpecialPowers.
|
||||
Cc["@mozilla.org/xre/app-info;1"].
|
||||
getService(content.SpecialPowers.Ci.nsIXULRuntime).
|
||||
widgetToolkit;
|
||||
let nativeMsg = widgetToolkit == "cocoa" ? 5 : // NSMouseMoved
|
||||
widgetToolkit == "windows" ? 1 : // MOUSEEVENTF_MOVE
|
||||
3; // GDK_MOTION_NOTIFY
|
||||
|
||||
row.addEventListener("mousemove", function onMove() {
|
||||
row.removeEventListener("mousemove", onMove);
|
||||
ack();
|
||||
});
|
||||
utils.sendNativeMouseEvent(x * scale, y * scale, nativeMsg, 0, null);
|
||||
},
|
||||
|
||||
mousedown: function (suggestionIdx) {
|
||||
gController.onClick = () => {
|
||||
gController.onClick = null;
|
||||
ack();
|
||||
};
|
||||
let row = gController._table.children[suggestionIdx];
|
||||
content.sendMouseEvent({ type: "mousedown" }, row);
|
||||
},
|
||||
|
||||
addInputValueToFormHistory: function () {
|
||||
gController.addInputValueToFormHistory();
|
||||
ack();
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
// Reset both the input and suggestions by select all + delete.
|
||||
gController.input.focus();
|
||||
content.synthesizeKey("a", { accelKey: true });
|
||||
content.synthesizeKey("VK_DELETE", {});
|
||||
ack();
|
||||
},
|
||||
};
|
||||
|
||||
function ack() {
|
||||
sendAsyncMessage(TEST_MSG, currentState());
|
||||
}
|
||||
|
||||
function waitForSuggestions(cb) {
|
||||
let observer = new content.MutationObserver(() => {
|
||||
if (gController.input.getAttribute("aria-expanded") == "true") {
|
||||
observer.disconnect();
|
||||
cb();
|
||||
}
|
||||
});
|
||||
observer.observe(gController.input, {
|
||||
attributes: true,
|
||||
attributeFilter: ["aria-expanded"],
|
||||
});
|
||||
}
|
||||
|
||||
function currentState() {
|
||||
let state = {
|
||||
selectedIndex: gController.selectedIndex,
|
||||
numSuggestions: gController.numSuggestions,
|
||||
suggestionAtIndex: [],
|
||||
isFormHistorySuggestionAtIndex: [],
|
||||
|
||||
tableHidden: gController._table.hidden,
|
||||
tableChildrenLength: gController._table.children.length,
|
||||
tableChildren: [],
|
||||
|
||||
inputValue: gController.input.value,
|
||||
ariaExpanded: gController.input.getAttribute("aria-expanded"),
|
||||
};
|
||||
|
||||
for (let i = 0; i < gController.numSuggestions; i++) {
|
||||
state.suggestionAtIndex.push(gController.suggestionAtIndex(i));
|
||||
state.isFormHistorySuggestionAtIndex.push(
|
||||
gController.isFormHistorySuggestionAtIndex(i));
|
||||
}
|
||||
|
||||
for (let child of gController._table.children) {
|
||||
state.tableChildren.push({
|
||||
textContent: child.textContent,
|
||||
classes: new Set(child.className.split(/\s+/)),
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
})();
|
|
@ -76,6 +76,13 @@ let runTaskifiedTests = Task.async(function* () {
|
|||
info("Adding search event listener");
|
||||
getContentWindow().addEventListener(SERVICE_EVENT_NAME, searchEventListener);
|
||||
|
||||
let panel = searchPanel();
|
||||
is(panel.state, "closed", "Search panel should be closed initially");
|
||||
|
||||
// The panel's animation often is not finished when the test clicks on panel
|
||||
// children, which makes the test click the wrong children, so disable it.
|
||||
panel.setAttribute("animate", "false");
|
||||
|
||||
// Add the engine without any logos and switch to it.
|
||||
let noLogoEngine = yield promiseNewSearchEngine(ENGINE_NO_LOGO);
|
||||
Services.search.currentEngine = noLogoEngine;
|
||||
|
@ -106,6 +113,19 @@ let runTaskifiedTests = Task.async(function* () {
|
|||
yield promiseSearchEvents(["CurrentEngine"]);
|
||||
yield checkCurrentEngine(ENGINE_1X_2X_LOGO);
|
||||
|
||||
// Click the logo to open the search panel.
|
||||
yield Promise.all([
|
||||
promisePanelShown(panel),
|
||||
promiseClick(logoImg()),
|
||||
]);
|
||||
|
||||
let manageBox = $("manage");
|
||||
ok(!!manageBox, "The Manage Engines box should be present in the document");
|
||||
is(panel.childNodes.length, 1, "Search panel should only contain the Manage Engines entry");
|
||||
is(panel.childNodes[0], manageBox, "Search panel should contain the Manage Engines entry");
|
||||
|
||||
panel.hidePopup();
|
||||
|
||||
// Add the engine that provides search suggestions and switch to it.
|
||||
let suggestionEngine = yield promiseNewSearchEngine(ENGINE_SUGGESTIONS);
|
||||
Services.search.currentEngine = suggestionEngine;
|
||||
|
@ -113,7 +133,7 @@ let runTaskifiedTests = Task.async(function* () {
|
|||
yield checkCurrentEngine(ENGINE_SUGGESTIONS);
|
||||
|
||||
// Avoid intermittent failures.
|
||||
gSearch().remoteTimeout = 5000;
|
||||
gSearch()._suggestionController.remoteTimeout = 5000;
|
||||
|
||||
// Type an X in the search input. This is only a smoke test. See
|
||||
// browser_searchSuggestionUI.js for comprehensive content search suggestion
|
||||
|
@ -289,10 +309,21 @@ let checkCurrentEngine = Task.async(function* ({name: basename, logoPrefix1x, lo
|
|||
" basename=" + basename);
|
||||
|
||||
// gSearch.currentEngineName
|
||||
is(gSearch().defaultEngine.name, engine.name,
|
||||
is(gSearch().currentEngineName, engine.name,
|
||||
"currentEngineName: " + engine.name);
|
||||
});
|
||||
|
||||
function promisePanelShown(panel) {
|
||||
let deferred = Promise.defer();
|
||||
info("Waiting for popupshown");
|
||||
panel.addEventListener("popupshown", function onEvent() {
|
||||
panel.removeEventListener("popupshown", onEvent);
|
||||
is(panel.state, "open", "Panel state");
|
||||
deferred.resolve();
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promiseClick(node) {
|
||||
let deferred = Promise.defer();
|
||||
let win = getContentWindow();
|
||||
|
@ -303,12 +334,16 @@ function promiseClick(node) {
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
function searchPanel() {
|
||||
return $("panel");
|
||||
}
|
||||
|
||||
function logoImg() {
|
||||
return $("logo");
|
||||
}
|
||||
|
||||
function gSearch() {
|
||||
return getContentWindow().gSearch._contentSearchController;
|
||||
return getContentWindow().gSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -722,23 +722,14 @@ function whenPagesUpdated(aCallback = TestRunner.next) {
|
|||
*/
|
||||
function whenSearchInitDone() {
|
||||
let deferred = Promise.defer();
|
||||
let searchController = getContentWindow().gSearch._contentSearchController;
|
||||
if (searchController.defaultEngine) {
|
||||
if (getContentWindow().gSearch._initialStateReceived) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
let eventName = "ContentSearchService";
|
||||
getContentWindow().addEventListener(eventName, function onEvent(event) {
|
||||
if (event.detail.type == "State") {
|
||||
getContentWindow().removeEventListener(eventName, onEvent);
|
||||
// Wait for the search controller to receive the event, then resolve.
|
||||
let resolver = function() {
|
||||
if (searchController.defaultEngine) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
}
|
||||
executeSoon(resolver);
|
||||
}
|
||||
executeSoon(resolver);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
|
|
|
@ -144,8 +144,8 @@ browser.jar:
|
|||
* content/browser/sanitize.xul (content/sanitize.xul)
|
||||
* content/browser/sanitizeDialog.js (content/sanitizeDialog.js)
|
||||
content/browser/sanitizeDialog.css (content/sanitizeDialog.css)
|
||||
content/browser/contentSearchUI.js (content/contentSearchUI.js)
|
||||
content/browser/contentSearchUI.css (content/contentSearchUI.css)
|
||||
content/browser/searchSuggestionUI.js (content/searchSuggestionUI.js)
|
||||
content/browser/searchSuggestionUI.css (content/searchSuggestionUI.css)
|
||||
content/browser/tabbrowser.css (content/tabbrowser.css)
|
||||
content/browser/tabbrowser.xml (content/tabbrowser.xml)
|
||||
content/browser/urlbarBindings.xml (content/urlbarBindings.xml)
|
||||
|
|
|
@ -30,9 +30,7 @@ add_task(function* test_mozLoop_getSelectedTabMetadata() {
|
|||
Assert.strictEqual(metadata.url, null, "URL should be empty for about:home");
|
||||
Assert.strictEqual(metadata.favicon, null, "Favicon should be empty for about:home");
|
||||
Assert.ok(metadata.title, "Title should be set for about:home");
|
||||
// Filter out null elements in the previews - contentSearchUI adds some img
|
||||
// elements with chrome:// srcs, which show up as null in metadata.previews.
|
||||
Assert.deepEqual(metadata.previews.filter(e => e), [], "No previews available for about:home");
|
||||
Assert.deepEqual(metadata.previews, [], "No previews available for about:home");
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<!ENTITY abouthome.pageTitle "&brandFullName; Start Page">
|
||||
|
||||
<!ENTITY abouthome.search.placeholder "Search">
|
||||
<!ENTITY abouthome.searchEngineButton.label "Search">
|
||||
|
||||
<!-- LOCALIZATION NOTE (abouthome.defaultSnippet1.v1):
|
||||
text in <a/> will be linked to the Firefox features page on mozilla.com
|
||||
|
|
|
@ -421,12 +421,6 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
|||
<!ENTITY searchFocus.commandkey2 "e">
|
||||
<!ENTITY searchFocusUnix.commandkey "j">
|
||||
|
||||
<!-- LOCALIZATION NOTE (contentSearchInput.label, contentSearchSubmit.label):
|
||||
These are set as the aria-label attribute for the search input box and
|
||||
submit button in the in-content search UI, to be used by screen readers. -->
|
||||
<!ENTITY contentSearchInput.label "Search query">
|
||||
<!ENTITY contentSearchSubmit.label "Submit search">
|
||||
|
||||
<!-- LOCALIZATION NOTE (searchFor.label, searchWith.label):
|
||||
These two strings are used to build the header above the list of one-click
|
||||
search providers: "Search for <used typed keywords> with:" -->
|
||||
|
|
|
@ -32,15 +32,3 @@ cmd_addFoundEngine=Add "%S"
|
|||
# search panel using the cmd_addFoundEngine string, they will be
|
||||
# grouped in a submenu using cmd_addFoundEngineMenu as a label.
|
||||
cmd_addFoundEngineMenu=Add search engine
|
||||
|
||||
# LOCALIZATION NOTE (searchFor, searchWith):
|
||||
# These two strings are used to build the header above the list of one-click
|
||||
# search providers: "Search for <user-typed keywords> with:"
|
||||
searchFor=Search for
|
||||
searchWith= with:
|
||||
|
||||
# LOCALIZATION NOTE (searchWithHeader):
|
||||
# The wording of this string should be as close as possible to
|
||||
# searchFor and searchWith. This string will be used instead of
|
||||
# them when the user has not typed any keyword.
|
||||
searchWithHeader=Search with:
|
||||
|
|
|
@ -100,6 +100,8 @@ let AboutHome = {
|
|||
"AboutHome:Sync",
|
||||
"AboutHome:Settings",
|
||||
"AboutHome:RequestUpdate",
|
||||
"AboutHome:Search",
|
||||
"AboutHome:OpenSearchPanel",
|
||||
],
|
||||
|
||||
init: function() {
|
||||
|
@ -108,6 +110,16 @@ let AboutHome = {
|
|||
for (let msg of this.MESSAGES) {
|
||||
mm.addMessageListener(msg, this);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(this, "browser-search-engine-modified", false);
|
||||
},
|
||||
|
||||
observe: function(aEngine, aTopic, aVerb) {
|
||||
switch (aTopic) {
|
||||
case "browser-search-engine-modified":
|
||||
this.sendAboutHomeData(null);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
|
@ -167,6 +179,65 @@ let AboutHome = {
|
|||
case "AboutHome:RequestUpdate":
|
||||
this.sendAboutHomeData(aMessage.target);
|
||||
break;
|
||||
|
||||
case "AboutHome:Search":
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(aMessage.data.searchData);
|
||||
} catch(ex) {
|
||||
Cu.reportError(ex);
|
||||
break;
|
||||
}
|
||||
|
||||
Services.search.init(function(status) {
|
||||
if (!Components.isSuccessCode(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let engine = Services.search.currentEngine;
|
||||
if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
|
||||
window.BrowserSearch.recordSearchInHealthReport(engine, "abouthome", data.selection);
|
||||
}
|
||||
|
||||
// Trigger a search through nsISearchEngine.getSubmission()
|
||||
let submission = engine.getSubmission(data.searchTerms, null, "homepage");
|
||||
let where = window.whereToOpenLink(data.originalEvent);
|
||||
|
||||
// There is a chance that by the time we receive the search message, the
|
||||
// user has switched away from the tab that triggered the search. If,
|
||||
// based on the event, we need to load the search in the same tab that
|
||||
// triggered it (i.e. where == "current"), openUILinkIn will not work
|
||||
// because that tab is no longer the current one. For this case we
|
||||
// manually load the URI in the target browser.
|
||||
if (where == "current") {
|
||||
aMessage.target.loadURIWithFlags(submission.uri.spec,
|
||||
Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
|
||||
null, null, submission.postData);
|
||||
} else {
|
||||
let params = {
|
||||
postData: submission.postData,
|
||||
inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
|
||||
};
|
||||
window.openLinkIn(submission.uri.spec, where, params);
|
||||
}
|
||||
// Used for testing
|
||||
let mm = aMessage.target.messageManager;
|
||||
mm.sendAsyncMessage("AboutHome:SearchTriggered", aMessage.data.searchData);
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "AboutHome:OpenSearchPanel":
|
||||
let panel = window.document.getElementById("abouthome-search-panel");
|
||||
let anchor = aMessage.objects.anchor;
|
||||
panel.hidden = false;
|
||||
panel.openPopup(anchor);
|
||||
anchor.setAttribute("active", "true");
|
||||
panel.addEventListener("popuphidden", function onHidden() {
|
||||
panel.removeEventListener("popuphidden", onHidden);
|
||||
anchor.removeAttribute("active");
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -179,11 +250,24 @@ let AboutHome = {
|
|||
let ss = wrapper.SessionStore;
|
||||
|
||||
ss.promiseInitialized.then(function() {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
Services.search.init(function (status){
|
||||
if (!Components.isSuccessCode(status)) {
|
||||
deferred.reject(status);
|
||||
} else {
|
||||
deferred.resolve(Services.search.defaultEngine.name);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}).then(function(engineName) {
|
||||
let data = {
|
||||
showRestoreLastSession: ss.canRestoreLastSession,
|
||||
snippetsURL: AboutHomeUtils.snippetsURL,
|
||||
showKnowYourRights: AboutHomeUtils.showKnowYourRights,
|
||||
snippetsVersion: AboutHomeUtils.snippetsVersion,
|
||||
defaultEngineName: engineName
|
||||
};
|
||||
|
||||
if (AboutHomeUtils.showKnowYourRights) {
|
||||
|
@ -201,5 +285,14 @@ let AboutHome = {
|
|||
}).then(null, function onError(x) {
|
||||
Cu.reportError("Error in AboutHome.sendAboutHomeData: " + x);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Focuses the search input in the page with the given message manager.
|
||||
* @param messageManager
|
||||
* The MessageManager object of the selected browser.
|
||||
*/
|
||||
focusInput: function (messageManager) {
|
||||
messageManager.sendAsyncMessage("AboutHome:FocusInput");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -45,9 +45,6 @@ const MAX_SUGGESTIONS = 6;
|
|||
* GetState
|
||||
* Retrieves the current search engine state.
|
||||
* data: null
|
||||
* GetStrings
|
||||
* Retrieves localized search UI strings.
|
||||
* data: null
|
||||
* ManageEngines
|
||||
* Opens the search engine management window.
|
||||
* data: null
|
||||
|
@ -56,7 +53,7 @@ const MAX_SUGGESTIONS = 6;
|
|||
* data: the entry, a string
|
||||
* Search
|
||||
* Performs a search.
|
||||
* data: { engineName, searchString, healthReportKey, searchPurpose }
|
||||
* data: { engineName, searchString, whence }
|
||||
* SetCurrentEngine
|
||||
* Sets the current engine.
|
||||
* data: the name of the engine
|
||||
|
@ -75,9 +72,6 @@ const MAX_SUGGESTIONS = 6;
|
|||
* State
|
||||
* Sent in reply to GetState.
|
||||
* data: see _currentStateObj
|
||||
* Strings
|
||||
* Sent in reply to GetStrings
|
||||
* data: Object containing string names and values for the current locale.
|
||||
* Suggestions
|
||||
* Sent in reply to GetSuggestions.
|
||||
* data: see _onMessageGetSuggestions
|
||||
|
@ -104,24 +98,9 @@ this.ContentSearch = {
|
|||
addMessageListener(INBOUND_MESSAGE, this);
|
||||
Services.obs.addObserver(this, "browser-search-engine-modified", false);
|
||||
Services.obs.addObserver(this, "shutdown-leaks-before-check", false);
|
||||
Services.prefs.addObserver("browser.search.hiddenOneOffs", this, false);
|
||||
this._stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
|
||||
},
|
||||
|
||||
get searchSuggestionUIStrings() {
|
||||
if (this._searchSuggestionUIStrings) {
|
||||
return this._searchSuggestionUIStrings;
|
||||
}
|
||||
this._searchSuggestionUIStrings = {};
|
||||
let searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties");
|
||||
let stringNames = ["searchHeader", "searchPlaceholder", "searchFor",
|
||||
"searchWith", "searchWithHeader"];
|
||||
for (let name of stringNames) {
|
||||
this._searchSuggestionUIStrings[name] = searchBundle.GetStringFromName(name);
|
||||
}
|
||||
return this._searchSuggestionUIStrings;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (this._destroyedPromise) {
|
||||
return this._destroyedPromise;
|
||||
|
@ -174,7 +153,6 @@ this.ContentSearch = {
|
|||
|
||||
observe: function (subj, topic, data) {
|
||||
switch (topic) {
|
||||
case "nsPref:changed":
|
||||
case "browser-search-engine-modified":
|
||||
this._eventQueue.push({
|
||||
type: "Observe",
|
||||
|
@ -223,19 +201,14 @@ this.ContentSearch = {
|
|||
});
|
||||
},
|
||||
|
||||
_onMessageGetStrings: function (msg, data) {
|
||||
this._reply(msg, "Strings", this.searchSuggestionUIStrings);
|
||||
},
|
||||
|
||||
_onMessageSearch: function (msg, data) {
|
||||
this._ensureDataHasProperties(data, [
|
||||
"engineName",
|
||||
"searchString",
|
||||
"healthReportKey",
|
||||
"searchPurpose",
|
||||
"whence",
|
||||
]);
|
||||
let engine = Services.search.getEngineByName(data.engineName);
|
||||
let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
|
||||
let submission = engine.getSubmission(data.searchString, "", data.whence);
|
||||
let browser = msg.target;
|
||||
let win;
|
||||
try {
|
||||
|
@ -266,7 +239,7 @@ this.ContentSearch = {
|
|||
};
|
||||
win.openUILinkIn(submission.uri.spec, where, params);
|
||||
}
|
||||
win.BrowserSearch.recordSearchInHealthReport(engine, data.healthReportKey,
|
||||
win.BrowserSearch.recordSearchInHealthReport(engine, data.whence,
|
||||
data.selection || null);
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
@ -441,12 +414,7 @@ this.ContentSearch = {
|
|||
engines: [],
|
||||
currentEngine: yield this._currentEngineObj(),
|
||||
};
|
||||
let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
|
||||
let hiddenList = pref ? pref.split(",") : [];
|
||||
for (let engine of Services.search.getVisibleEngines()) {
|
||||
if (hiddenList.indexOf(engine.name) != -1) {
|
||||
continue;
|
||||
}
|
||||
let uri = engine.getIconURLBySize(16, 16);
|
||||
state.engines.push({
|
||||
name: engine.name,
|
||||
|
|
|
@ -673,7 +673,7 @@ let DirectoryLinksProvider = {
|
|||
let pastImpressions;
|
||||
// Check if the suggested tile was shown
|
||||
if (action == "view") {
|
||||
sites.slice(0, triggeringSiteIndex + 1).filter(s => s).forEach(site => {
|
||||
sites.slice(0, triggeringSiteIndex + 1).forEach(site => {
|
||||
let {targetedSite, url} = site.link;
|
||||
if (targetedSite) {
|
||||
this._addFrequencyCapView(url);
|
||||
|
|
|
@ -94,8 +94,7 @@ add_task(function* search() {
|
|||
let data = {
|
||||
engineName: engine.name,
|
||||
searchString: "ContentSearchTest",
|
||||
healthReportKey: "ContentSearchTest",
|
||||
searchPurpose: "ContentSearchTest",
|
||||
whence: "ContentSearchTest",
|
||||
};
|
||||
gMsgMan.sendAsyncMessage(TEST_MSG, {
|
||||
type: "Search",
|
||||
|
@ -117,8 +116,7 @@ add_task(function* searchInBackgroundTab() {
|
|||
let data = {
|
||||
engineName: engine.name,
|
||||
searchString: "ContentSearchTest",
|
||||
healthReportKey: "ContentSearchTest",
|
||||
searchPurpose: "ContentSearchTest",
|
||||
whence: "ContentSearchTest",
|
||||
};
|
||||
gMsgMan.sendAsyncMessage(TEST_MSG, {
|
||||
type: "Search",
|
||||
|
|
|
@ -93,8 +93,6 @@ browser.jar:
|
|||
skin/classic/browser/badge-add-engine.png (../shared/search/badge-add-engine.png)
|
||||
skin/classic/browser/search-indicator-badge-add.png (../shared/search/search-indicator-badge-add.png)
|
||||
skin/classic/browser/search-history-icon.svg (../shared/search/history-icon.svg)
|
||||
skin/classic/browser/search-indicator-magnifying-glass.svg (../shared/search/search-indicator-magnifying-glass.svg)
|
||||
skin/classic/browser/search-arrow-go.svg (../shared/search/search-arrow-go.svg)
|
||||
skin/classic/browser/Security-broken.png
|
||||
skin/classic/browser/setDesktopBackground.css
|
||||
skin/classic/browser/slowStartup-16.png
|
||||
|
|
|
@ -124,8 +124,6 @@ browser.jar:
|
|||
skin/classic/browser/search-indicator-badge-add.png (../shared/search/search-indicator-badge-add.png)
|
||||
skin/classic/browser/search-indicator-badge-add@2x.png (../shared/search/search-indicator-badge-add@2x.png)
|
||||
skin/classic/browser/search-history-icon.svg (../shared/search/history-icon.svg)
|
||||
skin/classic/browser/search-indicator-magnifying-glass.svg (../shared/search/search-indicator-magnifying-glass.svg)
|
||||
skin/classic/browser/search-arrow-go.svg (../shared/search/search-arrow-go.svg)
|
||||
skin/classic/browser/slowStartup-16.png
|
||||
skin/classic/browser/theme-switcher-icon.png (../shared/theme-switcher-icon.png)
|
||||
skin/classic/browser/theme-switcher-icon@2x.png (../shared/theme-switcher-icon@2x.png)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
use:not(:target) {
|
||||
display: none;
|
||||
}
|
||||
use {
|
||||
fill: #616366;
|
||||
}
|
||||
use[id$="-inverted"] {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<path id="search-arrow-go-glyph" d="M1,7v2.2C1,9.8,1.4,10,2,10h7.5l-3,3.1c-0.4,0.3-0.4,1,0,1.4l0.8,0.8 c0.4,0.4,1,0.4,1.4,0l6.6-6.6c0.4-0.4,0.4-1,0-1.4L8.7,0.7c-0.4-0.4-1-0.4-1.4,0L6.5,1.6C6.1,2,6.1,2.6,6.5,3l3,3H2C1.4,6,1,6.4,1,7z"/>
|
||||
</defs>
|
||||
<use id="search-arrow-go" xlink:href="#search-arrow-go-glyph"/>
|
||||
<use id="search-arrow-go-inverted" xlink:href="#search-arrow-go-glyph"/>
|
||||
</svg>
|
До Ширина: | Высота: | Размер: 926 B |
|
@ -1,7 +0,0 @@
|
|||
<?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="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#808080" d="M21.7,20.3l-1.4,1.4l-5.4-5.4c-1.3,1-3,1.7-4.9,1.7 c-4.4,0-8-3.6-8-8c0-4.4,3.6-8,8-8c4.4,0,8,3.6,8,8c0,1.8-0.6,3.5-1.7,4.9L21.7,20.3z M10,4c-3.3,0-6,2.7-6,6s2.7,6,6,6s6-2.7,6-6 S13.3,4,10,4z"/>
|
||||
</svg>
|
До Ширина: | Высота: | Размер: 561 B |
|
@ -117,8 +117,6 @@ browser.jar:
|
|||
skin/classic/browser/search-indicator-badge-add.png (../shared/search/search-indicator-badge-add.png)
|
||||
skin/classic/browser/search-indicator-badge-add@2x.png (../shared/search/search-indicator-badge-add@2x.png)
|
||||
skin/classic/browser/search-history-icon.svg (../shared/search/history-icon.svg)
|
||||
skin/classic/browser/search-indicator-magnifying-glass.svg (../shared/search/search-indicator-magnifying-glass.svg)
|
||||
skin/classic/browser/search-arrow-go.svg (../shared/search/search-arrow-go.svg)
|
||||
skin/classic/browser/setDesktopBackground.css
|
||||
skin/classic/browser/slowStartup-16.png
|
||||
skin/classic/browser/theme-switcher-icon.png (../shared/theme-switcher-icon.png)
|
||||
|
|
Загрузка…
Ссылка в новой задаче