diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 03e4952f24b3..1a8ba291032d 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -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");
diff --git a/browser/base/content/contentSearchUI.css b/browser/base/content/contentSearchUI.css
deleted file mode 100644
index ee691e711a31..000000000000
--- a/browser/base/content/contentSearchUI.css
+++ /dev/null
@@ -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%);
-}
diff --git a/browser/base/content/contentSearchUI.js b/browser/base/content/contentSearchUI.js
deleted file mode 100644
index 9a62375bdbc4..000000000000
--- a/browser/base/content/contentSearchUI.js
+++ /dev/null
@@ -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;
-})();
diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css
index 3952d9db6e1c..d3e3ac93d9bd 100644
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -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;
}
diff --git a/browser/base/content/newtab/newTab.xul b/browser/base/content/newtab/newTab.xul
index a1736aa6c8fd..4868bbf8b2d5 100644
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -5,13 +5,15 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
+
%newTabDTD;
+
+ %searchBarDTD;
%browserDTD;
]>
@@ -20,6 +22,13 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&newtab.pageTitle;">
+
-
+ maxlength="256" dir="auto"/>
+
+
@@ -124,7 +133,7 @@
+ src="chrome://browser/content/searchSuggestionUI.js"/>
diff --git a/browser/base/content/newtab/search.js b/browser/base/content/newtab/search.js
index bb3eaf9c0d7f..6c8e20e711e6 100644
--- a/browser/base/content/newtab/search.js
+++ b/browser/base/content/newtab/search.js
@@ -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;
},
};
diff --git a/browser/base/content/searchSuggestionUI.css b/browser/base/content/searchSuggestionUI.css
new file mode 100644
index 000000000000..7e8d83a46f38
--- /dev/null
+++ b/browser/base/content/searchSuggestionUI.css
@@ -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;
+}
diff --git a/browser/base/content/searchSuggestionUI.js b/browser/base/content/searchSuggestionUI.js
new file mode 100644
index 000000000000..512fb5d80a30
--- /dev/null
+++ b/browser/base/content/searchSuggestionUI.js
@@ -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;
+})();
diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js
index 671935ded69f..2249aaa001d2 100644
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -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);
diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini
index 7ebf716167e5..65962699f0f0 100644
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -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]
diff --git a/browser/base/content/test/general/browser_aboutHome.js b/browser/base/content/test/general/browser_aboutHome.js
index 7f6addd4aaaf..9efd0e68e0f7 100644
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.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();
diff --git a/browser/base/content/test/general/browser_contentSearchUI.js b/browser/base/content/test/general/browser_contentSearchUI.js
deleted file mode 100644
index b4506c3f2a57..000000000000
--- a/browser/base/content/test/general/browser_contentSearchUI.js
+++ /dev/null
@@ -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;
-}
diff --git a/browser/base/content/test/general/browser_searchSuggestionUI.js b/browser/base/content/test/general/browser_searchSuggestionUI.js
new file mode 100644
index 000000000000..44df969e1403
--- /dev/null
+++ b/browser/base/content/test/general/browser_searchSuggestionUI.js
@@ -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;
+}
diff --git a/browser/base/content/test/general/contentSearchUI.js b/browser/base/content/test/general/contentSearchUI.js
deleted file mode 100644
index e37eb2b0fedf..000000000000
--- a/browser/base/content/test/general/contentSearchUI.js
+++ /dev/null
@@ -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;
-}
-
-})();
diff --git a/browser/base/content/test/general/searchSuggestionEngine.xml b/browser/base/content/test/general/searchSuggestionEngine.xml
index adcffa8f3149..c9c1f637dfcf 100644
--- a/browser/base/content/test/general/searchSuggestionEngine.xml
+++ b/browser/base/content/test/general/searchSuggestionEngine.xml
@@ -5,5 +5,5 @@
browser_searchSuggestionEngine searchSuggestionEngine.xml
-
+
diff --git a/browser/base/content/test/general/searchSuggestionEngine2.xml b/browser/base/content/test/general/searchSuggestionEngine2.xml
deleted file mode 100644
index 05644649a537..000000000000
--- a/browser/base/content/test/general/searchSuggestionEngine2.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-browser_searchSuggestionEngine searchSuggestionEngine2.xml
-
-
-
diff --git a/browser/base/content/test/general/contentSearchUI.html b/browser/base/content/test/general/searchSuggestionUI.html
similarity index 64%
rename from browser/base/content/test/general/contentSearchUI.html
rename to browser/base/content/test/general/searchSuggestionUI.html
index 3750ac2b0ce4..394294c859b6 100644
--- a/browser/base/content/test/general/contentSearchUI.html
+++ b/browser/base/content/test/general/searchSuggestionUI.html
@@ -9,13 +9,12 @@
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
-
-
+