mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-10 21:28:04 +02:00
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
480 lines
15 KiB
JavaScript
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);
|
|
}
|