304 lines
9.3 KiB
JavaScript
304 lines
9.3 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";
|
|
|
|
// Wrap in a block to prevent leaking to window scope.
|
|
{
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
BrowserSearchTelemetry:
|
|
"moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
|
|
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
|
SearchOneOffs: "moz-src:///browser/components/search/SearchOneOffs.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* A richlistbox popup custom element for for a browser search autocomplete
|
|
* widget.
|
|
*/
|
|
class MozSearchAutocompleteRichlistboxPopup extends MozElements.MozAutocompleteRichlistboxPopup {
|
|
constructor() {
|
|
super();
|
|
|
|
this.addEventListener("popupshowing", () => {
|
|
// First handle deciding if we are showing the reduced version of the
|
|
// popup containing only the preferences button. We do this if the
|
|
// glass icon has been clicked if the text field is empty.
|
|
if (this.searchbar.hasAttribute("showonlysettings")) {
|
|
this.searchbar.removeAttribute("showonlysettings");
|
|
this.setAttribute("showonlysettings", "true");
|
|
|
|
// Setting this with an xbl-inherited attribute gets overridden the
|
|
// second time the user clicks the glass icon for some reason...
|
|
this.richlistbox.collapsed = true;
|
|
} else {
|
|
this.removeAttribute("showonlysettings");
|
|
// Uncollapse as long as we have a view which has >= 1 row.
|
|
// The autocomplete binding itself will take care of uncollapsing later,
|
|
// if we currently have no rows but end up having some in the future
|
|
// when the search string changes
|
|
this.richlistbox.collapsed = this.matchCount == 0;
|
|
}
|
|
|
|
// Show the current default engine in the top header of the panel.
|
|
this.updateHeader().catch(console.error);
|
|
|
|
this._oneOffButtons.addEventListener(
|
|
"SelectedOneOffButtonChanged",
|
|
this
|
|
);
|
|
});
|
|
|
|
this.addEventListener("popuphiding", () => {
|
|
this._oneOffButtons.removeEventListener(
|
|
"SelectedOneOffButtonChanged",
|
|
this
|
|
);
|
|
});
|
|
|
|
/**
|
|
* This handles clicks on the topmost "Foo Search" header in the
|
|
* popup (hbox.search-panel-header]).
|
|
*/
|
|
this.addEventListener("click", event => {
|
|
if (event.button == 2) {
|
|
// Ignore right clicks.
|
|
return;
|
|
}
|
|
let button = event.originalTarget;
|
|
let engine = button.parentNode.engine;
|
|
if (!engine) {
|
|
return;
|
|
}
|
|
if (this.searchbar.value) {
|
|
this.oneOffButtons.handleSearchCommand(event, engine);
|
|
} else if (event.shiftKey) {
|
|
this.openSearchForm(event, engine);
|
|
}
|
|
});
|
|
|
|
this._bundle = null;
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
".search-panel-current-engine": "showonlysettings",
|
|
".searchbar-engine-image": "src",
|
|
};
|
|
}
|
|
|
|
// We override this because even though we have a shadow root, we want our
|
|
// inheritance to be done on the light tree.
|
|
getElementForAttrInheritance(selector) {
|
|
return this.querySelector(selector);
|
|
}
|
|
|
|
initialize() {
|
|
super.initialize();
|
|
this.initializeAttributeInheritance();
|
|
|
|
this._searchOneOffsContainer = this.querySelector(".search-one-offs");
|
|
this._searchbarEngine = this.querySelector(".search-panel-header");
|
|
this._searchbarEngineName = this.querySelector(".searchbar-engine-name");
|
|
this._oneOffButtons = new SearchOneOffs(this._searchOneOffsContainer);
|
|
this._searchbar = document.getElementById("searchbar");
|
|
}
|
|
|
|
get oneOffButtons() {
|
|
if (!this._oneOffButtons) {
|
|
this.initialize();
|
|
}
|
|
return this._oneOffButtons;
|
|
}
|
|
|
|
static get markup() {
|
|
return `
|
|
<hbox class="search-panel-header search-panel-current-engine">
|
|
<image class="searchbar-engine-image"/>
|
|
<label class="searchbar-engine-name" flex="1" crop="end" role="presentation"/>
|
|
</hbox>
|
|
<menuseparator class="searchbar-separator"/>
|
|
<richlistbox class="autocomplete-richlistbox search-panel-tree"/>
|
|
<menuseparator class="searchbar-separator"/>
|
|
<hbox class="search-one-offs" is_searchbar="true"/>
|
|
`;
|
|
}
|
|
|
|
get searchOneOffsContainer() {
|
|
if (!this._searchOneOffsContainer) {
|
|
this.initialize();
|
|
}
|
|
return this._searchOneOffsContainer;
|
|
}
|
|
|
|
get searchbarEngine() {
|
|
if (!this._searchbarEngine) {
|
|
this.initialize();
|
|
}
|
|
return this._searchbarEngine;
|
|
}
|
|
|
|
get searchbarEngineName() {
|
|
if (!this._searchbarEngineName) {
|
|
this.initialize();
|
|
}
|
|
return this._searchbarEngineName;
|
|
}
|
|
|
|
get searchbar() {
|
|
if (!this._searchbar) {
|
|
this.initialize();
|
|
}
|
|
return this._searchbar;
|
|
}
|
|
|
|
get bundle() {
|
|
if (!this._bundle) {
|
|
const kBundleURI = "chrome://browser/locale/search.properties";
|
|
this._bundle = Services.strings.createBundle(kBundleURI);
|
|
}
|
|
return this._bundle;
|
|
}
|
|
|
|
openAutocompletePopup(aInput, aElement) {
|
|
// initially the panel is hidden
|
|
// to avoid impacting startup / new window performance
|
|
aInput.popup.hidden = false;
|
|
|
|
// this method is defined on the base binding
|
|
this._openAutocompletePopup(aInput, aElement);
|
|
}
|
|
|
|
onPopupClick(aEvent) {
|
|
// Ignore all right-clicks
|
|
if (aEvent.button == 2) {
|
|
return;
|
|
}
|
|
|
|
this.searchbar.telemetrySelectedIndex = this.selectedIndex;
|
|
|
|
// Check for unmodified left-click, and use default behavior
|
|
if (
|
|
aEvent.button == 0 &&
|
|
!aEvent.shiftKey &&
|
|
!aEvent.ctrlKey &&
|
|
!aEvent.altKey &&
|
|
!aEvent.metaKey
|
|
) {
|
|
this.input.controller.handleEnter(true, aEvent);
|
|
return;
|
|
}
|
|
|
|
// Check for middle-click or modified clicks on the search bar
|
|
BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod(
|
|
aEvent,
|
|
"searchbar",
|
|
this.selectedIndex
|
|
);
|
|
|
|
// Handle search bar popup clicks
|
|
let search = this.input.controller.getValueAt(this.selectedIndex);
|
|
|
|
// open the search results according to the clicking subtlety
|
|
let where = BrowserUtils.whereToOpenLink(aEvent, false, true);
|
|
let params = {};
|
|
|
|
// But open ctrl/cmd clicks on autocomplete items in a new background tab.
|
|
let modifier =
|
|
AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
|
|
if (
|
|
where == "tab" &&
|
|
MouseEvent.isInstance(aEvent) &&
|
|
(aEvent.button == 1 || modifier)
|
|
) {
|
|
params.inBackground = true;
|
|
}
|
|
|
|
// leave the popup open for background tab loads
|
|
if (!(where == "tab" && params.inBackground)) {
|
|
// close the autocomplete popup and revert the entered search term
|
|
this.closePopup();
|
|
this.input.controller.handleEscape();
|
|
}
|
|
|
|
this.searchbar.doSearch(search, where, null, params);
|
|
if (where == "tab" && params.inBackground) {
|
|
this.searchbar.focus();
|
|
} else {
|
|
this.searchbar.value = search;
|
|
}
|
|
}
|
|
|
|
async updateHeader(engine) {
|
|
if (!engine) {
|
|
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
|
|
engine = await Services.search.getDefaultPrivate();
|
|
} else {
|
|
engine = await Services.search.getDefault();
|
|
}
|
|
}
|
|
|
|
let uri = await engine.getIconURL();
|
|
if (uri) {
|
|
this.setAttribute("src", uri);
|
|
} else {
|
|
// If the default has just been changed to a provider without icon,
|
|
// avoid showing the icon of the previous default provider.
|
|
this.removeAttribute("src");
|
|
}
|
|
|
|
let headerText = this.bundle.formatStringFromName("searchHeader", [
|
|
engine.name,
|
|
]);
|
|
this.searchbarEngineName.setAttribute("value", headerText);
|
|
this.searchbarEngine.engine = engine;
|
|
}
|
|
|
|
/**
|
|
* This is called when a one-off is clicked and when "search in new tab"
|
|
* is selected from a one-off context menu.
|
|
*/
|
|
/* eslint-disable-next-line valid-jsdoc */
|
|
handleOneOffSearch(event, engine, where, params) {
|
|
this.searchbar.handleSearchCommandWhere(event, engine, where, params);
|
|
}
|
|
|
|
openSearchForm(event, engine, forceNewTab = false) {
|
|
let { where, params } = this.oneOffButtons._whereToOpen(
|
|
event,
|
|
forceNewTab
|
|
);
|
|
this.searchbar.openSearchFormWhere(event, engine, where, params);
|
|
}
|
|
|
|
/**
|
|
* Passes DOM events for the popup to the _on_<event type> methods.
|
|
*
|
|
* @param {Event} event
|
|
* DOM event from the <popup>.
|
|
*/
|
|
handleEvent(event) {
|
|
let methodName = "_on_" + event.type;
|
|
if (methodName in this) {
|
|
this[methodName](event);
|
|
} else {
|
|
throw new Error("Unrecognized UrlbarView event: " + event.type);
|
|
}
|
|
}
|
|
_on_SelectedOneOffButtonChanged() {
|
|
let engine =
|
|
this.oneOffButtons.selectedButton &&
|
|
this.oneOffButtons.selectedButton.engine;
|
|
this.updateHeader(engine).catch(console.error);
|
|
}
|
|
}
|
|
|
|
customElements.define(
|
|
"search-autocomplete-richlistbox-popup",
|
|
MozSearchAutocompleteRichlistboxPopup,
|
|
{
|
|
extends: "panel",
|
|
}
|
|
);
|
|
}
|