fune/browser/components/search/content/searchbar.js
Mike de Boer 4e948732c1 Bug 1525833 - nsISearchService::addEngine does not support using a callback anymore, instead it returns a Promise. Update the searchbar's usage to reflect this. r=florian
This also officially gets rid of the nsIBrowserSearchInitObserver and nsISearchInstallCallback
from nsISearchService.idl, even though they're not used for anything anymore.

Differential Revision: https://phabricator.services.mozilla.com/D18993

--HG--
extra : moz-landing-system : lando
2019-02-07 16:17:54 +00:00

480 lines
15 KiB
JavaScript

/* 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";
/* globals XULCommandEvent */
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
const inheritsMap = {
".searchbar-textbox": ["disabled", "disableautocomplete", "searchengine", "src", "newlines"],
".searchbar-search-button": ["addengines"],
};
function inheritAttribute(parent, child, attr) {
if (!parent.hasAttribute(attr)) {
child.removeAttribute(attr);
} else {
child.setAttribute(attr, parent.getAttribute(attr));
}
}
/**
* Defines the search bar element.
*/
class MozSearchbar extends MozXULElement {
static get observedAttributes() {
let unique = new Set();
for (let i in inheritsMap) {
inheritsMap[i].forEach(attr => unique.add(attr));
}
return Array.from(unique);
}
attributeChangedCallback() {
this.inheritAttributes();
}
inheritAttributes() {
if (!this.isConnected) {
return;
}
for (let sel in inheritsMap) {
let node = this.querySelector(sel);
for (let attr of inheritsMap[sel]) {
inheritAttribute(this, node, attr);
}
}
}
constructor() {
super();
this.destroy = this.destroy.bind(this);
this._setupEventListeners();
let searchbar = this;
this.observer = {
observe(aEngine, aTopic, aVerb) {
if (aTopic == "browser-search-engine-modified" ||
(aTopic == "browser-search-service" && aVerb == "init-complete")) {
// Make sure the engine list is refetched next time it's needed
searchbar._engines = null;
// Update the popup header and update the display after any modification.
searchbar._textbox.popup.updateHeader();
searchbar.updateDisplay();
}
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
};
this.content = MozXULElement.parseXULToFragment(`
<stringbundle src="chrome://browser/locale/search.properties"></stringbundle>
<textbox class="searchbar-textbox" type="autocomplete" inputtype="search" placeholder="&searchInput.placeholder;" flex="1" autocompletepopup="PopupSearchAutoComplete" autocompletesearch="search-autocomplete" autocompletesearchparam="searchbar-history" maxrows="10" completeselectedindex="true" minresultsforpopup="0" inherits="disabled,disableautocomplete,searchengine,src,newlines">
<box>
<hbox class="searchbar-search-button" inherits="addengines" tooltiptext="&searchIcon.tooltip;">
<image class="searchbar-search-icon"></image>
<image class="searchbar-search-icon-overlay"></image>
</hbox>
</box>
<hbox class="search-go-container">
<image class="search-go-button urlbar-icon" hidden="true" onclick="handleSearchCommand(event);" tooltiptext="&contentSearchSubmit.tooltip;"></image>
</hbox>
</textbox>
`, ["chrome://browser/locale/browser.dtd"]);
}
connectedCallback() {
// Don't initialize if this isn't going to be visible
if (this.closest("#BrowserToolbarPalette")) {
return;
}
this.appendChild(document.importNode(this.content, true));
this.inheritAttributes();
window.addEventListener("unload", this.destroy);
this._ignoreFocus = false;
this._clickClosedPopup = false;
this._stringBundle = this.querySelector("stringbundle");
this._textboxInitialized = false;
this._textbox = this.querySelector(".searchbar-textbox");
this._engines = null;
this.FormHistory = (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
return;
Services.obs.addObserver(this.observer, "browser-search-engine-modified");
Services.obs.addObserver(this.observer, "browser-search-service");
this._initialized = true;
(window.delayedStartupPromise || Promise.resolve()).then(() => {
window.requestIdleCallback(() => {
Services.search.init().then(aStatus => {
// Bail out if the binding's been destroyed
if (!this._initialized)
return;
// Refresh the display (updating icon, etc)
this.updateDisplay();
BrowserSearch.updateOpenSearchBadge();
}).catch(status => Cu.reportError("Cannot initialize search service, bailing out: " + status));
});
});
// Wait until the popupshowing event to avoid forcing immediate
// attachment of the search-one-offs binding.
this.textbox.popup.addEventListener("popupshowing", () => {
let oneOffButtons = this.textbox.popup.oneOffButtons;
// Some accessibility tests create their own <searchbar> that doesn't
// use the popup binding below, so null-check oneOffButtons.
if (oneOffButtons) {
oneOffButtons.telemetryOrigin = "searchbar";
// Set .textbox first, since the popup setter will cause
// a _rebuild call that uses it.
oneOffButtons.textbox = this.textbox;
oneOffButtons.popup = this.textbox.popup;
}
}, { capture: true, once: true });
}
get engines() {
if (!this._engines)
this._engines = Services.search.getVisibleEngines();
return this._engines;
}
set currentEngine(val) {
Services.search.defaultEngine = val;
return val;
}
get currentEngine() {
let currentEngine = Services.search.defaultEngine;
// Return a dummy engine if there is no currentEngine
return currentEngine || { name: "", uri: null };
}
/**
* textbox is used by sanitize.js to clear the undo history when
* clearing form information.
*/
get textbox() {
return this._textbox;
}
set value(val) {
return this._textbox.value = val;
}
get value() {
return this._textbox.value;
}
destroy() {
if (this._initialized) {
this._initialized = false;
window.removeEventListener("unload", this.destroy);
Services.obs.removeObserver(this.observer, "browser-search-engine-modified");
Services.obs.removeObserver(this.observer, "browser-search-service");
}
// Make sure to break the cycle from _textbox to us. Otherwise we leak
// the world. But make sure it's actually pointing to us.
// Also make sure the textbox has ever been constructed, otherwise the
// _textbox getter will cause the textbox constructor to run, add an
// observer, and leak the world too.
if (this._textboxInitialized && this._textbox.mController.input == this)
this._textbox.mController.input = null;
}
focus() {
this._textbox.focus();
}
select() {
this._textbox.select();
}
setIcon(element, uri) {
element.setAttribute("src", uri);
}
updateDisplay() {
let uri = this.currentEngine.iconURI;
this.setIcon(this, uri ? uri.spec : "");
let name = this.currentEngine.name;
let text = this._stringBundle.getFormattedString("searchtip", [name]);
this._textbox.label = text;
this._textbox.tooltipText = text;
}
updateGoButtonVisibility() {
this.querySelector(".search-go-button").hidden = !this._textbox.value;
}
openSuggestionsPanel(aShowOnlySettingsIfEmpty) {
if (this._textbox.open)
return;
this._textbox.showHistoryPopup();
if (this._textbox.value) {
// showHistoryPopup does a startSearch("") call, ensure the
// controller handles the text from the input box instead:
this._textbox.mController.handleText();
} else if (aShowOnlySettingsIfEmpty) {
this.setAttribute("showonlysettings", "true");
}
}
selectEngine(aEvent, isNextEngine) {
// Find the new index
let newIndex = this.engines.indexOf(this.currentEngine);
newIndex += isNextEngine ? 1 : -1;
if (newIndex >= 0 && newIndex < this.engines.length) {
this.currentEngine = this.engines[newIndex];
}
aEvent.preventDefault();
aEvent.stopPropagation();
this.openSuggestionsPanel();
}
handleSearchCommand(aEvent, aEngine, aForceNewTab) {
let where = "current";
let params;
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
if (aEvent && aEvent.originalTarget.classList.contains("search-go-button")) {
if (aEvent.button == 2)
return;
where = whereToOpenLink(aEvent, false, true);
} else if (aForceNewTab) {
where = "tab";
if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
where += "-background";
} else {
let newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
if (((aEvent instanceof KeyboardEvent && aEvent.altKey) ^ newTabPref) &&
!gBrowser.selectedTab.isEmpty) {
where = "tab";
}
if ((aEvent instanceof MouseEvent) &&
(aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
where = "tab";
params = {
inBackground: true,
};
}
}
this.handleSearchCommandWhere(aEvent, aEngine, where, params);
}
handleSearchCommandWhere(aEvent, aEngine, aWhere, aParams) {
let textBox = this._textbox;
let textValue = textBox.value;
let selection = this.telemetrySearchDetails;
let oneOffRecorded = false;
BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
aEvent,
selection ? selection.index : -1
);
if (!selection || (selection.index == -1)) {
oneOffRecorded = this.textbox.popup.oneOffButtons
.maybeRecordTelemetry(aEvent);
if (!oneOffRecorded) {
let source = "unknown";
let type = "unknown";
let target = aEvent.originalTarget;
if (aEvent instanceof KeyboardEvent) {
type = "key";
} else if (aEvent instanceof MouseEvent) {
type = "mouse";
if (target.classList.contains("search-panel-header") ||
target.parentNode.classList.contains("search-panel-header")) {
source = "header";
}
} else if (aEvent instanceof XULCommandEvent) {
if (target.getAttribute("anonid") == "paste-and-search") {
source = "paste";
}
}
if (!aEngine) {
aEngine = this.currentEngine;
}
BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type);
}
}
// This is a one-off search only if oneOffRecorded is true.
this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
if (aWhere == "tab" && aParams && aParams.inBackground)
this.focus();
}
doSearch(aData, aWhere, aEngine, aParams, aOneOff) {
let textBox = this._textbox;
// Save the current value in the form history
if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
this.FormHistory.update({
op: "bump",
fieldname: textBox.getAttribute("autocompletesearchparam"),
value: aData,
}, {
handleError(aError) {
Cu.reportError("Saving search to form history failed: " + aError.message);
},
});
}
let engine = aEngine || this.currentEngine;
let submission = engine.getSubmission(aData, null, "searchbar");
let telemetrySearchDetails = this.telemetrySearchDetails;
this.telemetrySearchDetails = null;
if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
telemetrySearchDetails = null;
}
// If we hit here, we come either from a one-off, a plain search or a suggestion.
const details = {
isOneOff: aOneOff,
isSuggestion: (!aOneOff && telemetrySearchDetails),
selection: telemetrySearchDetails,
};
BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
// null parameter below specifies HTML response for search
let params = {
postData: submission.postData,
};
if (aParams) {
for (let key in aParams) {
params[key] = aParams[key];
}
}
openTrustedLinkIn(submission.uri.spec, aWhere, params);
}
disconnectedCallback() {
this.destroy();
while (this.firstChild) {
this.firstChild.remove();
}
}
_setupEventListeners() {
this.addEventListener("command", (event) => {
const target = event.originalTarget;
if (target.engine) {
this.currentEngine = target.engine;
} else if (target.classList.contains("addengine-item")) {
// Select the installed engine if the installation succeeds.
Services.search.addEngine(target.getAttribute("uri"), null,
target.getAttribute("src"), false).then(engine => this.currentEngine = engine);
} else
return;
this.focus();
this.select();
});
this.addEventListener("DOMMouseScroll", (event) => {
if (event.getModifierState("Accel")) {
this.selectEngine(event, event.detail > 0);
}
}, true);
this.addEventListener("input", (event) => { this.updateGoButtonVisibility(); });
this.addEventListener("drop", (event) => { this.updateGoButtonVisibility(); });
this.addEventListener("blur", (event) => {
// If the input field is still focused then a different window has
// received focus, ignore the next focus event.
this._ignoreFocus = (document.activeElement == this._textbox.inputField);
}, true);
this.addEventListener("focus", (event) => {
// Speculatively connect to the current engine's search URI (and
// suggest URI, if different) to reduce request latency
this.currentEngine.speculativeConnect({
window,
originAttributes: gBrowser.contentPrincipal
.originAttributes,
});
if (this._ignoreFocus) {
// This window has been re-focused, don't show the suggestions
this._ignoreFocus = false;
return;
}
// Don't open the suggestions if there is no text in the textbox.
if (!this._textbox.value)
return;
// Don't open the suggestions if the mouse was used to focus the
// textbox, that will be taken care of in the click handler.
if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
return;
this.openSuggestionsPanel();
}, true);
this.addEventListener("mousedown", (event) => {
if (event.originalTarget.classList.contains("searchbar-search-button")) {
this._clickClosedPopup = this._textbox.popup._isHiding;
}
}, true);
this.addEventListener("mousedown", (event) => {
// Ignore right clicks
if (event.button != 0) {
return;
}
// Ignore clicks on the search go button.
if (event.originalTarget.classList.contains("search-go-button")) {
return;
}
// Ignore clicks on menu items in the input's context menu.
if (event.originalTarget.localName == "menuitem") {
return;
}
let isIconClick = event.originalTarget.classList.contains("searchbar-search-button");
// Ignore clicks on the icon if they were made to close the popup
if (isIconClick && this._clickClosedPopup) {
return;
}
// Open the suggestions whenever clicking on the search icon or if there
// is text in the textbox.
if (isIconClick || this._textbox.value) {
this.openSuggestionsPanel(true);
}
});
}
}
customElements.define("searchbar", MozSearchbar);
}