552 lines
15 KiB
JavaScript
552 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
OpenSearchManager:
|
|
"moz-src:///browser/components/search/OpenSearchManager.sys.mjs",
|
|
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
|
|
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
|
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
|
|
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "SearchModeSwitcherL10n", () => {
|
|
return new Localization(["browser/browser.ftl"]);
|
|
});
|
|
|
|
// The maximum number of openSearch engines available to install
|
|
// to display.
|
|
const MAX_OPENSEARCH_ENGINES = 3;
|
|
|
|
// Default icon used for engines that do not have icons loaded.
|
|
const DEFAULT_ENGINE_ICON =
|
|
"chrome://browser/skin/search-engine-placeholder@2x.png";
|
|
|
|
/**
|
|
* Implements the SearchModeSwitcher in the urlbar.
|
|
*/
|
|
export class SearchModeSwitcher {
|
|
static DEFAULT_ICON = lazy.UrlbarUtils.ICON.SEARCH_GLASS;
|
|
#popup;
|
|
#input;
|
|
#toolbarbutton;
|
|
|
|
constructor(input) {
|
|
this.#input = input;
|
|
|
|
this.QueryInterface = ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
]);
|
|
|
|
lazy.UrlbarPrefs.addObserver(this);
|
|
|
|
this.#popup = input.document.getElementById("searchmode-switcher-popup");
|
|
|
|
this.#toolbarbutton = input.document.querySelector(
|
|
"#urlbar-searchmode-switcher"
|
|
);
|
|
|
|
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
|
|
this.#enableObservers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open the SearchSwitcher popup.
|
|
*
|
|
* @param {Event} event
|
|
* The event that triggered the opening of the popup.
|
|
*/
|
|
async openPanel(event) {
|
|
if (
|
|
(event.type == "click" && event.button != 0) ||
|
|
(event.type == "keypress" &&
|
|
event.keyCode != KeyEvent.DOM_VK_RETURN &&
|
|
event.keyCode != KeyEvent.DOM_VK_DOWN)
|
|
) {
|
|
return; // Left click, down arrow or enter only
|
|
}
|
|
|
|
let anchor = event.target.closest("#urlbar-searchmode-switcher");
|
|
event.preventDefault();
|
|
|
|
if (this.#input.document.documentElement.hasAttribute("customizing")) {
|
|
return;
|
|
}
|
|
|
|
await this.#buildSearchModeList(this.#input.window);
|
|
|
|
this.#input.view.close({ showFocusBorder: false });
|
|
|
|
this.#popup.addEventListener(
|
|
"popuphidden",
|
|
() => {
|
|
anchor.removeAttribute("open");
|
|
anchor.setAttribute("aria-expanded", false);
|
|
},
|
|
{ once: true }
|
|
);
|
|
anchor.setAttribute("open", true);
|
|
anchor.setAttribute("aria-expanded", true);
|
|
|
|
if (event.type == "keypress") {
|
|
// If open the panel by key, set urlbar input filed as focusedElement to
|
|
// move the focus to the input field it when popup will be closed.
|
|
// Please see _prevFocus element in toolkit/content/widgets/panel.js about
|
|
// the implementation.
|
|
this.#input.document.commandDispatcher.focusedElement =
|
|
this.#input.inputField;
|
|
}
|
|
|
|
lazy.PanelMultiView.openPopup(this.#popup, anchor, {
|
|
position: "bottomleft topleft",
|
|
triggerEvent: event,
|
|
}).catch(console.error);
|
|
|
|
Glean.urlbarUnifiedsearchbutton.opened.add(1);
|
|
}
|
|
|
|
/**
|
|
* Close the SearchSwitcher popup.
|
|
*/
|
|
closePanel() {
|
|
this.#popup.hidePopup();
|
|
}
|
|
|
|
#openPreferences(event) {
|
|
if (
|
|
(event.type == "click" && event.button != 0) ||
|
|
(event.type == "keypress" &&
|
|
event.charCode != KeyEvent.DOM_VK_SPACE &&
|
|
event.keyCode != KeyEvent.DOM_VK_RETURN)
|
|
) {
|
|
return; // Left click, space or enter only
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
this.#input.window.openPreferences("paneSearch");
|
|
this.#popup.hidePopup();
|
|
|
|
Glean.urlbarUnifiedsearchbutton.picked.settings.add(1);
|
|
}
|
|
|
|
/**
|
|
* Exit the engine specific searchMode.
|
|
*
|
|
* @param {Event} event
|
|
* The event that triggered the searchMode exit.
|
|
*/
|
|
exitSearchMode(event) {
|
|
event.preventDefault();
|
|
this.#input.searchMode = null;
|
|
// Update the result by the default engine.
|
|
this.#input.startQuery();
|
|
}
|
|
|
|
/**
|
|
* Called when the value of the searchMode attribute on UrlbarInput is changed.
|
|
*/
|
|
onSearchModeChanged() {
|
|
if (!this.#input.window || this.#input.window.closed) {
|
|
return;
|
|
}
|
|
|
|
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
|
|
this.updateSearchIcon();
|
|
|
|
if (
|
|
this.#input.searchMode?.engineName == "Perplexity" &&
|
|
!lazy.UrlbarPrefs.get("perplexity.hasBeenInSearchMode")
|
|
) {
|
|
lazy.UrlbarPrefs.set("perplexity.hasBeenInSearchMode", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.type == "focus") {
|
|
this.#input.setUnifiedSearchButtonAvailability(true);
|
|
return;
|
|
}
|
|
|
|
if (this.#input.view.isOpen) {
|
|
// The urlbar view is opening, which means the unified search button got
|
|
// focus by tab key from urlbar.
|
|
switch (event.keyCode) {
|
|
case KeyEvent.DOM_VK_TAB: {
|
|
// Move the focus to urlbar view to make cyclable.
|
|
this.#input.focus();
|
|
this.#input.view.selectBy(1, {
|
|
reverse: event.shiftKey,
|
|
userPressedTab: true,
|
|
});
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
case KeyEvent.DOM_VK_ESCAPE: {
|
|
this.#input.view.close();
|
|
this.#input.focus();
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let action = event.currentTarget.dataset.action ?? event.type;
|
|
|
|
switch (action) {
|
|
case "openpopup": {
|
|
this.openPanel(event);
|
|
break;
|
|
}
|
|
case "exitsearchmode": {
|
|
this.exitSearchMode(event);
|
|
break;
|
|
}
|
|
case "openpreferences": {
|
|
this.#openPreferences(event);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
observe(_subject, topic, data) {
|
|
if (!this.#input.window || this.#input.window.closed) {
|
|
return;
|
|
}
|
|
|
|
switch (topic) {
|
|
case "browser-search-engine-modified": {
|
|
if (
|
|
data === "engine-default" ||
|
|
data === "engine-default-private" ||
|
|
data === "engine-icon-changed"
|
|
) {
|
|
this.updateSearchIcon();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a urlbar pref changes.
|
|
*
|
|
* @param {string} pref
|
|
* The name of the pref relative to `browser.urlbar`.
|
|
*/
|
|
onPrefChanged(pref) {
|
|
if (!this.#input.window || this.#input.window.closed) {
|
|
return;
|
|
}
|
|
|
|
switch (pref) {
|
|
case "scotchBonnet.enableOverride": {
|
|
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
|
|
this.#enableObservers();
|
|
this.updateSearchIcon();
|
|
} else {
|
|
this.#disableObservers();
|
|
}
|
|
break;
|
|
}
|
|
case "keyword.enabled": {
|
|
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
|
|
this.updateSearchIcon();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateSearchIcon() {
|
|
let searchMode = this.#input.searchMode;
|
|
|
|
try {
|
|
await lazy.UrlbarSearchUtils.init();
|
|
} catch {
|
|
console.error("Search service failed to init");
|
|
}
|
|
|
|
let { label, icon } = await this.#getDisplayedEngineDetails(
|
|
this.#input.searchMode
|
|
);
|
|
|
|
if (searchMode?.source != this.#input.searchMode?.source) {
|
|
return;
|
|
}
|
|
|
|
const inSearchMode = this.#input.searchMode;
|
|
if (!lazy.UrlbarPrefs.get("unifiedSearchButton.always")) {
|
|
const keywordEnabled = lazy.UrlbarPrefs.get("keyword.enabled");
|
|
if (!keywordEnabled && !inSearchMode) {
|
|
icon = SearchModeSwitcher.DEFAULT_ICON;
|
|
}
|
|
} else if (!inSearchMode) {
|
|
// Use default icon set in CSS.
|
|
icon = null;
|
|
}
|
|
|
|
let iconUrl = icon ? `url(${icon})` : null;
|
|
this.#input.document.getElementById(
|
|
"searchmode-switcher-icon"
|
|
).style.listStyleImage = iconUrl;
|
|
|
|
if (label) {
|
|
this.#input.document.l10n.setAttributes(
|
|
this.#toolbarbutton,
|
|
"urlbar-searchmode-button2",
|
|
{ engine: label }
|
|
);
|
|
} else {
|
|
this.#input.document.l10n.setAttributes(
|
|
this.#toolbarbutton,
|
|
"urlbar-searchmode-button-no-engine"
|
|
);
|
|
}
|
|
|
|
let labelEl = this.#input.document.getElementById(
|
|
"searchmode-switcher-title"
|
|
);
|
|
|
|
if (!inSearchMode) {
|
|
labelEl.replaceChildren();
|
|
} else {
|
|
labelEl.textContent = label;
|
|
}
|
|
}
|
|
|
|
async #getSearchModeLabel(source) {
|
|
let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
|
|
m => m.source == source
|
|
);
|
|
let [str] = await lazy.SearchModeSwitcherL10n.formatMessages([
|
|
{ id: mode.uiLabel },
|
|
]);
|
|
return str.attributes[0].value;
|
|
}
|
|
|
|
async #getDisplayedEngineDetails(searchMode = null) {
|
|
if (!Services.search.hasSuccessfullyInitialized) {
|
|
return { label: null, icon: SearchModeSwitcher.DEFAULT_ICON };
|
|
}
|
|
|
|
if (!searchMode || searchMode.engineName) {
|
|
let engine = searchMode
|
|
? lazy.UrlbarSearchUtils.getEngineByName(searchMode.engineName)
|
|
: lazy.UrlbarSearchUtils.getDefaultEngine(
|
|
lazy.PrivateBrowsingUtils.isWindowPrivate(this.#input.window)
|
|
);
|
|
let icon = (await engine.getIconURL()) ?? SearchModeSwitcher.DEFAULT_ICON;
|
|
return { label: engine.name, icon };
|
|
}
|
|
|
|
let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
|
|
m => m.source == searchMode.source
|
|
);
|
|
return {
|
|
label: await this.#getSearchModeLabel(searchMode.source),
|
|
icon: mode.icon,
|
|
};
|
|
}
|
|
|
|
async #buildSearchModeList() {
|
|
// Remove all menuitems added.
|
|
for (let item of this.#popup.querySelectorAll(
|
|
".searchmode-switcher-addEngine, .searchmode-switcher-installed, .searchmode-switcher-local"
|
|
)) {
|
|
item.remove();
|
|
}
|
|
|
|
let browser = this.#input.window.gBrowser;
|
|
let separator = this.#popup.querySelector(
|
|
"#searchmode-switcher-popup-footer-separator"
|
|
);
|
|
|
|
let openSearchEngines = lazy.OpenSearchManager.getEngines(
|
|
browser.selectedBrowser
|
|
);
|
|
openSearchEngines = openSearchEngines.slice(0, MAX_OPENSEARCH_ENGINES);
|
|
|
|
for (let engine of openSearchEngines) {
|
|
let menuitem = this.#createButton(engine.title, engine.icon);
|
|
menuitem.classList.add("searchmode-switcher-addEngine");
|
|
menuitem.addEventListener("command", e => {
|
|
this.#installOpenSearchEngine(e, engine);
|
|
});
|
|
this.#popup.insertBefore(menuitem, separator);
|
|
}
|
|
|
|
// Add engines installed.
|
|
let engines = [];
|
|
try {
|
|
engines = await Services.search.getVisibleEngines();
|
|
} catch {
|
|
console.error("Failed to fetch engines");
|
|
}
|
|
|
|
for (let engine of engines) {
|
|
if (engine.hideOneOffButton) {
|
|
continue;
|
|
}
|
|
let icon = await engine.getIconURL();
|
|
let menuitem = this.#createButton(engine.name, icon);
|
|
menuitem.classList.add("searchmode-switcher-installed");
|
|
menuitem.setAttribute("label", engine.name);
|
|
menuitem.addEventListener("command", e => {
|
|
this.search({ engine, openEngineHomePage: e.shiftKey });
|
|
});
|
|
this.#popup.insertBefore(menuitem, separator);
|
|
}
|
|
|
|
// Add local options.
|
|
for (let { source, pref, restrict } of lazy.UrlbarUtils
|
|
.LOCAL_SEARCH_MODES) {
|
|
if (!lazy.UrlbarPrefs.get(pref)) {
|
|
continue;
|
|
}
|
|
let name = lazy.UrlbarUtils.getResultSourceName(source);
|
|
let { icon } = await this.#getDisplayedEngineDetails({
|
|
source,
|
|
pref,
|
|
restrict,
|
|
});
|
|
let menuitem = this.#createButton(name, icon);
|
|
menuitem.id = `search-button-${name}`;
|
|
menuitem.classList.add("searchmode-switcher-local");
|
|
menuitem.addEventListener("command", () => {
|
|
this.search({ restrict });
|
|
});
|
|
|
|
this.#input.document.l10n.setAttributes(
|
|
menuitem,
|
|
`urlbar-searchmode-${name}`,
|
|
{
|
|
restrict,
|
|
}
|
|
);
|
|
|
|
menuitem.restrict = restrict;
|
|
this.#popup.insertBefore(menuitem, separator);
|
|
}
|
|
}
|
|
|
|
search({ engine = null, restrict = null, openEngineHomePage = false } = {}) {
|
|
let gBrowser = this.#input.window.gBrowser;
|
|
let search = "";
|
|
let opts = null;
|
|
if (engine) {
|
|
let state = this.#input.getBrowserState(gBrowser.selectedBrowser);
|
|
search = gBrowser.userTypedValue ?? state.persist?.searchTerms ?? "";
|
|
opts = {
|
|
searchEngine: engine,
|
|
searchModeEntry: "searchbutton",
|
|
openEngineHomePage,
|
|
};
|
|
} else if (restrict) {
|
|
search = restrict + " " + (gBrowser.userTypedValue || "");
|
|
opts = { searchModeEntry: "searchbutton" };
|
|
}
|
|
|
|
if (openEngineHomePage) {
|
|
opts.focus = false;
|
|
opts.startQuery = false;
|
|
}
|
|
|
|
this.#input.search(search, opts);
|
|
|
|
if (openEngineHomePage) {
|
|
this.#input.openEngineHomePage(search, {
|
|
searchEngine: opts.searchEngine,
|
|
});
|
|
}
|
|
|
|
this.#popup.hidePopup();
|
|
|
|
if (engine) {
|
|
Glean.urlbarUnifiedsearchbutton.picked[
|
|
engine.isAppProvided ? "builtin_search" : "addon_search"
|
|
].add(1);
|
|
} else if (restrict) {
|
|
Glean.urlbarUnifiedsearchbutton.picked.local_search.add(1);
|
|
} else {
|
|
console.warn(
|
|
`Unexpected search: ${JSON.stringify({ engine, restrict, openEngineHomePage })}`
|
|
);
|
|
}
|
|
}
|
|
|
|
#enableObservers() {
|
|
Services.obs.addObserver(this, "browser-search-engine-modified", true);
|
|
|
|
this.#toolbarbutton.addEventListener("focus", this);
|
|
this.#toolbarbutton.addEventListener("command", this);
|
|
this.#toolbarbutton.addEventListener("keypress", this);
|
|
|
|
let closebutton = this.#input.document.querySelector(
|
|
"#searchmode-switcher-close"
|
|
);
|
|
closebutton.addEventListener("command", this);
|
|
closebutton.addEventListener("keypress", this);
|
|
|
|
let prefsbutton = this.#input.document.querySelector(
|
|
"#searchmode-switcher-popup-search-settings-button"
|
|
);
|
|
prefsbutton.addEventListener("command", this);
|
|
}
|
|
|
|
#disableObservers() {
|
|
Services.obs.removeObserver(this, "browser-search-engine-modified");
|
|
|
|
this.#toolbarbutton.removeEventListener("focus", this);
|
|
this.#toolbarbutton.removeEventListener("command", this);
|
|
this.#toolbarbutton.removeEventListener("keypress", this);
|
|
|
|
let closebutton = this.#input.document.querySelector(
|
|
"#searchmode-switcher-close"
|
|
);
|
|
closebutton.removeEventListener("command", this);
|
|
closebutton.removeEventListener("keypress", this);
|
|
|
|
let prefsbutton = this.#input.document.querySelector(
|
|
"#searchmode-switcher-popup-search-settings-button"
|
|
);
|
|
prefsbutton.removeEventListener("command", this);
|
|
}
|
|
|
|
#createButton(label, icon) {
|
|
let menuitem = this.#input.window.document.createXULElement("menuitem");
|
|
menuitem.setAttribute("label", label);
|
|
menuitem.setAttribute("class", "menuitem-iconic");
|
|
menuitem.setAttribute("image", icon ?? DEFAULT_ENGINE_ICON);
|
|
return menuitem;
|
|
}
|
|
|
|
async #installOpenSearchEngine(e, engine) {
|
|
let topic = "browser-search-engine-modified";
|
|
|
|
let observer = engineObj => {
|
|
Services.obs.removeObserver(observer, topic);
|
|
let eng = Services.search.getEngineByName(engineObj.wrappedJSObject.name);
|
|
this.search({
|
|
engine: eng,
|
|
openEngineHomePage: e.shiftKey,
|
|
});
|
|
};
|
|
Services.obs.addObserver(observer, topic);
|
|
|
|
await lazy.SearchUIUtils.addOpenSearchEngine(
|
|
engine.uri,
|
|
engine.icon,
|
|
this.#input.browsingContext
|
|
);
|
|
}
|
|
}
|