зеркало из https://github.com/mozilla/gecko-dev.git
2418 строки
95 KiB
XML
2418 строки
95 KiB
XML
<?xml version="1.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/. -->
|
|
|
|
<!-- This file is imported into the browser window. -->
|
|
<!-- eslint-env mozilla/browser-window -->
|
|
|
|
<!-- XULCommandEvent is a specialised global. -->
|
|
<!-- global XULCommandEvent -->
|
|
|
|
<!DOCTYPE bindings [
|
|
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
|
|
%browserDTD;
|
|
]>
|
|
|
|
<bindings id="SearchBindings"
|
|
xmlns="http://www.mozilla.org/xbl"
|
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
xmlns:xbl="http://www.mozilla.org/xbl">
|
|
|
|
<binding id="searchbar">
|
|
<resources>
|
|
<stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
|
|
<stylesheet src="chrome://browser/skin/searchbar.css"/>
|
|
</resources>
|
|
<content>
|
|
<xul:stringbundle src="chrome://browser/locale/search.properties"
|
|
anonid="searchbar-stringbundle"/>
|
|
<!--
|
|
There is a dependency between "maxrows" attribute and
|
|
"SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
|
|
one of them requires changing the other one.
|
|
-->
|
|
<xul:textbox class="searchbar-textbox"
|
|
anonid="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"
|
|
xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines">
|
|
<!--
|
|
Empty <box> to properly position the icon within the autocomplete
|
|
binding's anonymous children (the autocomplete binding positions <box>
|
|
children differently)
|
|
-->
|
|
<xul:box>
|
|
<xul:hbox class="searchbar-search-button"
|
|
anonid="searchbar-search-button"
|
|
xbl:inherits="addengines"
|
|
tooltiptext="&searchIcon.tooltip;">
|
|
<xul:image class="searchbar-search-icon"/>
|
|
<xul:image class="searchbar-search-icon-overlay"/>
|
|
</xul:hbox>
|
|
</xul:box>
|
|
<xul:hbox class="search-go-container">
|
|
<xul:image class="search-go-button urlbar-icon" hidden="true"
|
|
anonid="search-go-button"
|
|
onclick="handleSearchCommand(event);"
|
|
tooltiptext="&contentSearchSubmit.tooltip;"/>
|
|
</xul:hbox>
|
|
</xul:textbox>
|
|
</content>
|
|
|
|
<implementation implements="nsIObserver">
|
|
<constructor><![CDATA[
|
|
if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
|
|
return;
|
|
|
|
Services.obs.addObserver(this, "browser-search-engine-modified");
|
|
|
|
this._initialized = true;
|
|
|
|
(window.delayedStartupPromise || Promise.resolve()).then(() => {
|
|
window.requestIdleCallback(() => {
|
|
Services.search.init(aStatus => {
|
|
// Bail out if the binding's been destroyed
|
|
if (!this._initialized)
|
|
return;
|
|
|
|
if (Components.isSuccessCode(aStatus)) {
|
|
// Refresh the display (updating icon, etc)
|
|
this.updateDisplay();
|
|
BrowserSearch.updateOpenSearchBadge();
|
|
} else {
|
|
Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
}, {capturing: true, once: true});
|
|
]]></constructor>
|
|
|
|
<destructor><![CDATA[
|
|
this.destroy();
|
|
]]></destructor>
|
|
|
|
<method name="destroy">
|
|
<body><![CDATA[
|
|
if (this._initialized) {
|
|
this._initialized = false;
|
|
|
|
Services.obs.removeObserver(this, "browser-search-engine-modified");
|
|
}
|
|
|
|
// 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;
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_ignoreFocus">false</field>
|
|
<field name="_clickClosedPopup">false</field>
|
|
<field name="_stringBundle">document.getAnonymousElementByAttribute(this,
|
|
"anonid", "searchbar-stringbundle");</field>
|
|
<field name="_textboxInitialized">false</field>
|
|
<field name="_textbox">document.getAnonymousElementByAttribute(this,
|
|
"anonid", "searchbar-textbox");</field>
|
|
<field name="_engines">null</field>
|
|
<field name="FormHistory" readonly="true">
|
|
(Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
|
|
</field>
|
|
|
|
<property name="engines" readonly="true">
|
|
<getter><![CDATA[
|
|
if (!this._engines)
|
|
this._engines = Services.search.getVisibleEngines();
|
|
return this._engines;
|
|
]]></getter>
|
|
</property>
|
|
|
|
<property name="currentEngine">
|
|
<setter><![CDATA[
|
|
Services.search.currentEngine = val;
|
|
return val;
|
|
]]></setter>
|
|
<getter><![CDATA[
|
|
var currentEngine = Services.search.currentEngine;
|
|
// Return a dummy engine if there is no currentEngine
|
|
return currentEngine || {name: "", uri: null};
|
|
]]></getter>
|
|
</property>
|
|
|
|
<!-- textbox is used by sanitize.js to clear the undo history when
|
|
clearing form information. -->
|
|
<property name="textbox" readonly="true"
|
|
onget="return this._textbox;"/>
|
|
|
|
<property name="value" onget="return this._textbox.value;"
|
|
onset="return this._textbox.value = val;"/>
|
|
|
|
<method name="focus">
|
|
<body><![CDATA[
|
|
this._textbox.focus();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="select">
|
|
<body><![CDATA[
|
|
this._textbox.select();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="observe">
|
|
<parameter name="aEngine"/>
|
|
<parameter name="aTopic"/>
|
|
<parameter name="aVerb"/>
|
|
<body><![CDATA[
|
|
if (aTopic == "browser-search-engine-modified") {
|
|
switch (aVerb) {
|
|
case "engine-removed":
|
|
this.offerNewEngine(aEngine);
|
|
break;
|
|
case "engine-added":
|
|
this.hideNewEngine(aEngine);
|
|
break;
|
|
case "engine-changed":
|
|
// An engine was removed (or hidden) or added, or an icon was
|
|
// changed. Do nothing special.
|
|
}
|
|
|
|
// Make sure the engine list is refetched next time it's needed
|
|
this._engines = null;
|
|
|
|
// Update the popup header and update the display after any modification.
|
|
this._textbox.popup.updateHeader();
|
|
this.updateDisplay();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- There are two seaprate lists of search engines, whose uses intersect
|
|
in this file. The search service (nsIBrowserSearchService and
|
|
nsSearchService.js) maintains a list of Engine objects which is used to
|
|
populate the searchbox list of available engines and to perform queries.
|
|
That list is accessed here via this.SearchService, and it's that sort of
|
|
Engine that is passed to this binding's observer as aEngine.
|
|
|
|
In addition, browser.js fills two lists of autodetected search engines
|
|
(browser.engines and browser.hiddenEngines) as properties of
|
|
mCurrentBrowser. Those lists contain unnamed JS objects of the form
|
|
{ uri:, title:, icon: }, and that's what the searchbar uses to determine
|
|
whether to show any "Add <EngineName>" menu items in the drop-down.
|
|
|
|
The two types of engines are currently related by their identifying
|
|
titles (the Engine object's 'name'), although that may change; see bug
|
|
335102. -->
|
|
|
|
<!-- If the engine that was just removed from the searchbox list was
|
|
autodetected on this page, move it to each browser's active list so it
|
|
will be offered to be added again. -->
|
|
<method name="offerNewEngine">
|
|
<parameter name="aEngine"/>
|
|
<body><![CDATA[
|
|
for (let browser of gBrowser.browsers) {
|
|
if (browser.hiddenEngines) {
|
|
// XXX This will need to be changed when engines are identified by
|
|
// URL rather than title; see bug 335102.
|
|
var removeTitle = aEngine.wrappedJSObject.name;
|
|
for (var i = 0; i < browser.hiddenEngines.length; i++) {
|
|
if (browser.hiddenEngines[i].title == removeTitle) {
|
|
if (!browser.engines)
|
|
browser.engines = [];
|
|
browser.engines.push(browser.hiddenEngines[i]);
|
|
browser.hiddenEngines.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
BrowserSearch.updateOpenSearchBadge();
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- If the engine that was just added to the searchbox list was
|
|
autodetected on this page, move it to each browser's hidden list so it is
|
|
no longer offered to be added. -->
|
|
<method name="hideNewEngine">
|
|
<parameter name="aEngine"/>
|
|
<body><![CDATA[
|
|
for (let browser of gBrowser.browsers) {
|
|
if (browser.engines) {
|
|
// XXX This will need to be changed when engines are identified by
|
|
// URL rather than title; see bug 335102.
|
|
var removeTitle = aEngine.wrappedJSObject.name;
|
|
for (var i = 0; i < browser.engines.length; i++) {
|
|
if (browser.engines[i].title == removeTitle) {
|
|
if (!browser.hiddenEngines)
|
|
browser.hiddenEngines = [];
|
|
browser.hiddenEngines.push(browser.engines[i]);
|
|
browser.engines.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
BrowserSearch.updateOpenSearchBadge();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="setIcon">
|
|
<parameter name="element"/>
|
|
<parameter name="uri"/>
|
|
<body><![CDATA[
|
|
element.setAttribute("src", uri);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="updateDisplay">
|
|
<body><![CDATA[
|
|
var uri = this.currentEngine.iconURI;
|
|
this.setIcon(this, uri ? uri.spec : "");
|
|
|
|
var name = this.currentEngine.name;
|
|
var text = this._stringBundle.getFormattedString("searchtip", [name]);
|
|
this._textbox.label = text;
|
|
this._textbox.tooltipText = text;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="updateGoButtonVisibility">
|
|
<body><![CDATA[
|
|
document.getAnonymousElementByAttribute(this, "anonid",
|
|
"search-go-button")
|
|
.hidden = !this._textbox.value;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="openSuggestionsPanel">
|
|
<parameter name="aShowOnlySettingsIfEmpty"/>
|
|
<body><![CDATA[
|
|
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");
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="selectEngine">
|
|
<parameter name="aEvent"/>
|
|
<parameter name="isNextEngine"/>
|
|
<body><![CDATA[
|
|
// Find the new index
|
|
var 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();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="handleSearchCommand">
|
|
<parameter name="aEvent"/>
|
|
<parameter name="aEngine"/>
|
|
<parameter name="aForceNewTab"/>
|
|
<body><![CDATA[
|
|
var where = "current";
|
|
let params;
|
|
|
|
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
|
|
if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "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 {
|
|
var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
|
|
if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
|
|
where = "tab";
|
|
if ((aEvent instanceof MouseEvent) &&
|
|
(aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
|
|
where = "tab";
|
|
params = {
|
|
inBackground: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
this.handleSearchCommandWhere(aEvent, aEngine, where, params);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="handleSearchCommandWhere">
|
|
<parameter name="aEvent"/>
|
|
<parameter name="aEngine"/>
|
|
<parameter name="aWhere"/>
|
|
<parameter name="aParams"/>
|
|
<body><![CDATA[
|
|
var textBox = this._textbox;
|
|
var 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, aWhere, aParams);
|
|
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,
|
|
aWhere);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="doSearch">
|
|
<parameter name="aData"/>
|
|
<parameter name="aWhere"/>
|
|
<parameter name="aEngine"/>
|
|
<parameter name="aParams"/>
|
|
<parameter name="aOneOff"/>
|
|
<body><![CDATA[
|
|
var 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) {
|
|
Components.utils.reportError("Saving search to form history failed: " + aError.message);
|
|
}});
|
|
}
|
|
|
|
let engine = aEngine || this.currentEngine;
|
|
var 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];
|
|
}
|
|
}
|
|
openUILinkIn(submission.uri.spec, aWhere, params);
|
|
]]></body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="command"><![CDATA[
|
|
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
|
|
var installCallback = {
|
|
onSuccess: engine => this.currentEngine = engine
|
|
};
|
|
Services.search.addEngine(target.getAttribute("uri"), null,
|
|
target.getAttribute("src"), false,
|
|
installCallback);
|
|
} else
|
|
return;
|
|
|
|
this.focus();
|
|
this.select();
|
|
]]></handler>
|
|
|
|
<handler event="DOMMouseScroll"
|
|
phase="capturing"
|
|
modifiers="accel"
|
|
action="this.selectEngine(event, (event.detail > 0));"/>
|
|
|
|
<handler event="input" action="this.updateGoButtonVisibility();"/>
|
|
<handler event="drop" action="this.updateGoButtonVisibility();"/>
|
|
|
|
<handler event="blur">
|
|
<![CDATA[
|
|
// 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);
|
|
]]></handler>
|
|
|
|
<handler event="focus">
|
|
<![CDATA[
|
|
// 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();
|
|
]]></handler>
|
|
|
|
<handler event="mousedown" phase="capturing">
|
|
<![CDATA[
|
|
if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
|
|
this._clickClosedPopup = this._textbox.popup._isHiding;
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="mousedown" button="0">
|
|
<![CDATA[
|
|
// Ignore clicks on the search go button.
|
|
if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
|
|
return;
|
|
}
|
|
|
|
let isIconClick = event.originalTarget.getAttribute("anonid") == "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);
|
|
}
|
|
]]></handler>
|
|
|
|
</handlers>
|
|
</binding>
|
|
|
|
<binding id="searchbar-textbox"
|
|
extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
|
|
<implementation>
|
|
<constructor><![CDATA[
|
|
if (document.getBindingParent(this).parentNode.parentNode.localName ==
|
|
"toolbarpaletteitem")
|
|
return;
|
|
|
|
if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
|
|
this.setAttribute("clickSelectsAll", true);
|
|
|
|
var textBox = document.getAnonymousElementByAttribute(this,
|
|
"anonid", "textbox-input-box");
|
|
var cxmenu = document.getAnonymousElementByAttribute(textBox,
|
|
"anonid", "input-box-contextmenu");
|
|
cxmenu.addEventListener("popupshowing",
|
|
() => { this.initContextMenu(cxmenu); },
|
|
{capturing: true, once: true});
|
|
|
|
this.setAttribute("aria-owns", this.popup.id);
|
|
document.getBindingParent(this)._textboxInitialized = true;
|
|
]]></constructor>
|
|
|
|
<destructor><![CDATA[
|
|
// If the context menu has never been opened, there won't be anything
|
|
// to remove here.
|
|
// Also, XBL and the customize toolbar code sometimes interact poorly.
|
|
try {
|
|
this.controllers.removeController(this.searchbarController);
|
|
} catch (ex) { }
|
|
]]></destructor>
|
|
|
|
// Add items to context menu and attach controller to handle them the
|
|
// first time the context menu is opened.
|
|
<method name="initContextMenu">
|
|
<parameter name="aMenu"/>
|
|
<body><![CDATA[
|
|
const kXULNS =
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
let stringBundle = document.getBindingParent(this)._stringBundle;
|
|
|
|
let pasteAndSearch, suggestMenuItem;
|
|
let element, label, akey;
|
|
|
|
element = document.createElementNS(kXULNS, "menuseparator");
|
|
aMenu.appendChild(element);
|
|
|
|
let insertLocation = aMenu.firstChild;
|
|
while (insertLocation.nextSibling &&
|
|
insertLocation.getAttribute("cmd") != "cmd_paste")
|
|
insertLocation = insertLocation.nextSibling;
|
|
if (insertLocation) {
|
|
element = document.createElementNS(kXULNS, "menuitem");
|
|
label = stringBundle.getString("cmd_pasteAndSearch");
|
|
element.setAttribute("label", label);
|
|
element.setAttribute("anonid", "paste-and-search");
|
|
element.setAttribute("oncommand", "BrowserSearch.pasteAndSearch(event)");
|
|
aMenu.insertBefore(element, insertLocation.nextSibling);
|
|
pasteAndSearch = element;
|
|
}
|
|
|
|
element = document.createElementNS(kXULNS, "menuitem");
|
|
label = stringBundle.getString("cmd_clearHistory");
|
|
akey = stringBundle.getString("cmd_clearHistory_accesskey");
|
|
element.setAttribute("label", label);
|
|
element.setAttribute("accesskey", akey);
|
|
element.setAttribute("cmd", "cmd_clearhistory");
|
|
aMenu.appendChild(element);
|
|
|
|
element = document.createElementNS(kXULNS, "menuitem");
|
|
label = stringBundle.getString("cmd_showSuggestions");
|
|
akey = stringBundle.getString("cmd_showSuggestions_accesskey");
|
|
element.setAttribute("anonid", "toggle-suggest-item");
|
|
element.setAttribute("label", label);
|
|
element.setAttribute("accesskey", akey);
|
|
element.setAttribute("cmd", "cmd_togglesuggest");
|
|
element.setAttribute("type", "checkbox");
|
|
element.setAttribute("autocheck", "false");
|
|
suggestMenuItem = element;
|
|
aMenu.appendChild(element);
|
|
|
|
if (AppConstants.platform == "macosx") {
|
|
this.addEventListener("keypress", aEvent => {
|
|
if (aEvent.keyCode == KeyEvent.DOM_VK_F4)
|
|
this.openSearch();
|
|
}, true);
|
|
}
|
|
|
|
this.controllers.appendController(this.searchbarController);
|
|
|
|
let onpopupshowing = function() {
|
|
BrowserSearch.searchBar._textbox.closePopup();
|
|
if (suggestMenuItem) {
|
|
let enabled =
|
|
Services.prefs.getBoolPref("browser.search.suggest.enabled");
|
|
suggestMenuItem.setAttribute("checked", enabled);
|
|
}
|
|
|
|
if (!pasteAndSearch)
|
|
return;
|
|
let controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
|
|
let enabled = controller.isCommandEnabled("cmd_paste");
|
|
if (enabled)
|
|
pasteAndSearch.removeAttribute("disabled");
|
|
else
|
|
pasteAndSearch.setAttribute("disabled", "true");
|
|
};
|
|
aMenu.addEventListener("popupshowing", onpopupshowing);
|
|
onpopupshowing();
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
This overrides the searchParam property in autocomplete.xml. We're
|
|
hijacking this property as a vehicle for delivering the privacy
|
|
information about the window into the guts of nsSearchSuggestions.
|
|
|
|
Note that the setter is the same as the parent. We were not sure whether
|
|
we can override just the getter. If that proves to be the case, the setter
|
|
can be removed.
|
|
-->
|
|
<property name="searchParam"
|
|
onget="return this.getAttribute('autocompletesearchparam') +
|
|
(PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');"
|
|
onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
|
|
|
|
<!-- This is implemented so that when textbox.value is set directly (e.g.,
|
|
by tests), the one-off query is updated. -->
|
|
<method name="onBeforeValueSet">
|
|
<parameter name="aValue"/>
|
|
<body><![CDATA[
|
|
this.popup.oneOffButtons.query = aValue;
|
|
return aValue;
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
This method overrides the autocomplete binding's openPopup (essentially
|
|
duplicating the logic from the autocomplete popup binding's
|
|
openAutocompletePopup method), modifying it so that the popup is aligned with
|
|
the inner textbox, but sized to not extend beyond the search bar border.
|
|
-->
|
|
<method name="openPopup">
|
|
<body><![CDATA[
|
|
// Entering customization mode after the search bar had focus causes
|
|
// the popup to appear again, due to focus returning after the
|
|
// hamburger panel closes. Don't open in that spurious event.
|
|
if (document.documentElement.getAttribute("customizing") == "true") {
|
|
return;
|
|
}
|
|
|
|
var popup = this.popup;
|
|
if (!popup.mPopupOpen) {
|
|
// Initially the panel used for the searchbar (PopupSearchAutoComplete
|
|
// in browser.xul) is hidden to avoid impacting startup / new
|
|
// window performance. The base binding's openPopup would normally
|
|
// call the overriden openAutocompletePopup in
|
|
// browser-search-autocomplete-result-popup binding to unhide the popup,
|
|
// but since we're overriding openPopup we need to unhide the panel
|
|
// ourselves.
|
|
popup.hidden = false;
|
|
|
|
// Don't roll up on mouse click in the anchor for the search UI.
|
|
if (popup.id == "PopupSearchAutoComplete") {
|
|
popup.setAttribute("norolluponanchor", "true");
|
|
}
|
|
|
|
popup.mInput = this;
|
|
popup.view = this.controller.QueryInterface(Ci.nsITreeView);
|
|
popup.invalidate();
|
|
|
|
popup.showCommentColumn = this.showCommentColumn;
|
|
popup.showImageColumn = this.showImageColumn;
|
|
|
|
document.popupNode = null;
|
|
|
|
const isRTL = getComputedStyle(this, "").direction == "rtl";
|
|
|
|
var outerRect = this.getBoundingClientRect();
|
|
var innerRect = this.inputField.getBoundingClientRect();
|
|
let width = isRTL ?
|
|
innerRect.right - outerRect.left :
|
|
outerRect.right - innerRect.left;
|
|
popup.setAttribute("width", width > 100 ? width : 100);
|
|
|
|
var yOffset = outerRect.bottom - innerRect.bottom;
|
|
popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="openSearch">
|
|
<body>
|
|
<![CDATA[
|
|
if (!this.popupOpen) {
|
|
document.getBindingParent(this).openSuggestionsPanel();
|
|
return false;
|
|
}
|
|
return true;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleEnter">
|
|
<parameter name="event"/>
|
|
<body><![CDATA[
|
|
// Toggle the open state of the add-engine menu button if it's
|
|
// selected. We're using handleEnter for this instead of listening
|
|
// for the command event because a command event isn't fired.
|
|
if (this.selectedButton &&
|
|
this.selectedButton.getAttribute("anonid") ==
|
|
"addengine-menu-button") {
|
|
this.selectedButton.open = !this.selectedButton.open;
|
|
return true;
|
|
}
|
|
// Otherwise, "call super": do what the autocomplete binding's
|
|
// handleEnter implementation does.
|
|
return this.mController.handleEnter(false, event || null);
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- override |onTextEntered| in autocomplete.xml -->
|
|
<method name="onTextEntered">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
let engine;
|
|
let oneOff = this.selectedButton;
|
|
if (oneOff) {
|
|
if (!oneOff.engine) {
|
|
oneOff.doCommand();
|
|
return;
|
|
}
|
|
engine = oneOff.engine;
|
|
}
|
|
if (this._selectionDetails &&
|
|
this._selectionDetails.currentIndex != -1) {
|
|
BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
|
|
this._selectionDetails = null;
|
|
}
|
|
document.getBindingParent(this).handleSearchCommand(aEvent, engine);
|
|
]]></body>
|
|
</method>
|
|
|
|
<property name="selectedButton">
|
|
<getter><![CDATA[
|
|
return this.popup.oneOffButtons.selectedButton;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
return this.popup.oneOffButtons.selectedButton = val;
|
|
]]></setter>
|
|
</property>
|
|
|
|
<method name="handleKeyboardNavigation">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
let popup = this.popup;
|
|
if (!popup.popupOpen)
|
|
return;
|
|
|
|
// accel + up/down changes the default engine and shouldn't affect
|
|
// the selection on the one-off buttons.
|
|
if (aEvent.getModifierState("Accel"))
|
|
return;
|
|
|
|
let suggestionsHidden =
|
|
popup.tree.getAttribute("collapsed") == "true";
|
|
let numItems = suggestionsHidden ? 0 : this.popup.view.rowCount;
|
|
this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- nsIController -->
|
|
<field name="searchbarController" readonly="true"><![CDATA[({
|
|
_self: this,
|
|
supportsCommand(aCommand) {
|
|
return aCommand == "cmd_clearhistory" ||
|
|
aCommand == "cmd_togglesuggest";
|
|
},
|
|
|
|
isCommandEnabled(aCommand) {
|
|
return true;
|
|
},
|
|
|
|
doCommand(aCommand) {
|
|
switch (aCommand) {
|
|
case "cmd_clearhistory":
|
|
var param = this._self.getAttribute("autocompletesearchparam");
|
|
|
|
BrowserSearch.searchBar.FormHistory.update({ op: "remove", fieldname: param }, null);
|
|
this._self.value = "";
|
|
break;
|
|
case "cmd_togglesuggest":
|
|
let enabled =
|
|
Services.prefs.getBoolPref("browser.search.suggest.enabled");
|
|
Services.prefs.setBoolPref("browser.search.suggest.enabled",
|
|
!enabled);
|
|
break;
|
|
default:
|
|
// do nothing with unrecognized command
|
|
}
|
|
}
|
|
})]]></field>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="input"><![CDATA[
|
|
this.popup.removeAttribute("showonlysettings");
|
|
]]></handler>
|
|
|
|
<handler event="keypress" phase="capturing"
|
|
action="return this.handleKeyboardNavigation(event);"/>
|
|
|
|
<handler event="keypress" keycode="VK_UP" modifiers="accel"
|
|
phase="capturing"
|
|
action="document.getBindingParent(this).selectEngine(event, false);"/>
|
|
|
|
<handler event="keypress" keycode="VK_DOWN" modifiers="accel"
|
|
phase="capturing"
|
|
action="document.getBindingParent(this).selectEngine(event, true);"/>
|
|
|
|
<handler event="keypress" keycode="VK_DOWN" modifiers="alt"
|
|
phase="capturing"
|
|
action="return this.openSearch();"/>
|
|
|
|
<handler event="keypress" keycode="VK_UP" modifiers="alt"
|
|
phase="capturing"
|
|
action="return this.openSearch();"/>
|
|
|
|
<handler event="dragover">
|
|
<![CDATA[
|
|
var types = event.dataTransfer.types;
|
|
if (types.includes("text/plain") || types.includes("text/x-moz-text-internal"))
|
|
event.preventDefault();
|
|
]]>
|
|
</handler>
|
|
|
|
<handler event="drop">
|
|
<![CDATA[
|
|
var dataTransfer = event.dataTransfer;
|
|
var data = dataTransfer.getData("text/plain");
|
|
if (!data)
|
|
data = dataTransfer.getData("text/x-moz-text-internal");
|
|
if (data) {
|
|
event.preventDefault();
|
|
this.value = data;
|
|
document.getBindingParent(this).openSuggestionsPanel();
|
|
}
|
|
]]>
|
|
</handler>
|
|
|
|
</handlers>
|
|
</binding>
|
|
|
|
<binding id="browser-search-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup">
|
|
<resources>
|
|
<stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
|
|
<stylesheet src="chrome://browser/skin/searchbar.css"/>
|
|
</resources>
|
|
<content ignorekeys="true" level="top" consumeoutsideclicks="never">
|
|
<xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
|
|
class="search-panel-header search-panel-current-engine">
|
|
<xul:image class="searchbar-engine-image" xbl:inherits="src"/>
|
|
<xul:label anonid="searchbar-engine-name" flex="1" crop="end"
|
|
role="presentation"/>
|
|
</xul:hbox>
|
|
<xul:tree anonid="tree" flex="1"
|
|
class="autocomplete-tree plain search-panel-tree"
|
|
hidecolumnpicker="true" seltype="single">
|
|
<xul:treecols anonid="treecols">
|
|
<xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
|
|
</xul:treecols>
|
|
<xul:treechildren class="autocomplete-treebody searchbar-treebody" noSelectOnMouseMove="true"/>
|
|
</xul:tree>
|
|
<xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
|
|
</content>
|
|
<implementation>
|
|
<method name="openAutocompletePopup">
|
|
<parameter name="aInput"/>
|
|
<parameter name="aElement"/>
|
|
<body><![CDATA[
|
|
// 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);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="onPopupClick">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
// Ignore all right-clicks
|
|
if (aEvent.button == 2)
|
|
return;
|
|
|
|
var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
|
|
|
|
var searchBar = BrowserSearch.searchBar;
|
|
var popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
|
|
if (popupForSearchBar) {
|
|
searchBar.telemetrySearchDetails = {
|
|
index: controller.selection.currentIndex,
|
|
kind: "mouse"
|
|
};
|
|
}
|
|
|
|
// Check for unmodified left-click, and use default behavior
|
|
if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
|
|
!aEvent.altKey && !aEvent.metaKey) {
|
|
controller.handleEnter(true, aEvent);
|
|
return;
|
|
}
|
|
|
|
// Check for middle-click or modified clicks on the search bar
|
|
if (popupForSearchBar) {
|
|
BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
|
|
aEvent,
|
|
this.selectedIndex
|
|
);
|
|
|
|
// Handle search bar popup clicks
|
|
var search = controller.getValueAt(this.selectedIndex);
|
|
|
|
// open the search results according to the clicking subtlety
|
|
var where = 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" && (aEvent instanceof MouseEvent) &&
|
|
(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();
|
|
controller.handleEscape();
|
|
}
|
|
|
|
searchBar.doSearch(search, where, null, params);
|
|
if (where == "tab" && params.inBackground)
|
|
searchBar.focus();
|
|
else
|
|
searchBar.value = search;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- Popup rollup is triggered by native events before the mousedown event
|
|
reaches the DOM. The will be set to true by the popuphiding event and
|
|
false after the mousedown event has been triggered to detect what
|
|
caused rollup. -->
|
|
<field name="_isHiding">false</field>
|
|
<field name="_bundle">null</field>
|
|
<property name="bundle" readonly="true">
|
|
<getter>
|
|
<![CDATA[
|
|
if (!this._bundle) {
|
|
const kBundleURI = "chrome://browser/locale/search.properties";
|
|
this._bundle = Services.strings.createBundle(kBundleURI);
|
|
}
|
|
return this._bundle;
|
|
]]>
|
|
</getter>
|
|
</property>
|
|
|
|
<field name="oneOffButtons" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid",
|
|
"search-one-off-buttons");
|
|
</field>
|
|
|
|
<method name="updateHeader">
|
|
<body><![CDATA[
|
|
let currentEngine = Services.search.currentEngine;
|
|
let uri = currentEngine.iconURI;
|
|
if (uri) {
|
|
this.setAttribute("src", uri.spec);
|
|
} 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",
|
|
[currentEngine.name], 1);
|
|
document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
|
|
.setAttribute("value", headerText);
|
|
document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
|
|
.engine = currentEngine;
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- This is called when a one-off is clicked and when "search in new tab"
|
|
is selected from a one-off context menu. -->
|
|
<method name="handleOneOffSearch">
|
|
<parameter name="event"/>
|
|
<parameter name="engine"/>
|
|
<parameter name="where"/>
|
|
<parameter name="params"/>
|
|
<body><![CDATA[
|
|
let searchbar = document.getElementById("searchbar");
|
|
searchbar.handleSearchCommandWhere(event, engine, where, params);
|
|
]]></body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="popupshowing"><![CDATA[
|
|
// Force the panel to have the width of the searchbar rather than
|
|
// the width of the textfield.
|
|
let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
let textboxRect = DOMUtils.getBoundsWithoutFlushing(this.mInput);
|
|
let inputRect = DOMUtils.getBoundsWithoutFlushing(this.mInput.inputField);
|
|
|
|
// Ensure the panel is wide enough to fit at least 3 engines.
|
|
let minWidth = Math.max(textboxRect.width,
|
|
this.oneOffButtons.buttonWidth * 3);
|
|
this.style.minWidth = Math.round(minWidth) + "px";
|
|
// Alignment of the panel with the searchbar is obtained with negative
|
|
// margins.
|
|
this.style.marginLeft = (textboxRect.left - inputRect.left) + "px";
|
|
// This second margin is needed when the direction is reversed,
|
|
// eg. when using command+shift+X.
|
|
this.style.marginRight = (inputRect.right - textboxRect.right) + "px";
|
|
|
|
// 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.
|
|
let searchbar = document.getElementById("searchbar");
|
|
if (searchbar.hasAttribute("showonlysettings")) {
|
|
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.tree.collapsed = true;
|
|
} else {
|
|
this.removeAttribute("showonlysettings");
|
|
// Uncollapse as long as we have a tree with 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.tree.collapsed = !this.tree.view || !this.tree.view.rowCount;
|
|
}
|
|
|
|
// Show the current default engine in the top header of the panel.
|
|
this.updateHeader();
|
|
]]></handler>
|
|
|
|
<handler event="popuphiding"><![CDATA[
|
|
this._isHiding = true;
|
|
Services.tm.dispatchToMainThread(() => {
|
|
this._isHiding = false;
|
|
});
|
|
]]></handler>
|
|
|
|
<!-- This handles clicks on the topmost "Foo Search" header in the
|
|
popup (hbox[anonid="searchbar-engine"]). -->
|
|
<handler event="click"><![CDATA[
|
|
if (event.button == 2) {
|
|
// Ignore right clicks.
|
|
return;
|
|
}
|
|
let button = event.originalTarget;
|
|
let engine = button.parentNode.engine;
|
|
if (!engine) {
|
|
return;
|
|
}
|
|
this.oneOffButtons.handleSearchCommand(event, engine);
|
|
]]></handler>
|
|
</handlers>
|
|
|
|
</binding>
|
|
|
|
<!-- Used for additional open search providers in the search panel. -->
|
|
<binding id="addengine-icon" extends="xul:box">
|
|
<content>
|
|
<xul:image class="addengine-icon" xbl:inherits="src"/>
|
|
<xul:image class="addengine-badge"/>
|
|
</content>
|
|
</binding>
|
|
|
|
<binding id="search-one-offs">
|
|
<content context="_child">
|
|
<xul:deck anonid="search-panel-one-offs-header"
|
|
selectedIndex="0"
|
|
class="search-panel-header search-panel-current-input">
|
|
<xul:label anonid="searchbar-oneoffheader-search"
|
|
value="&searchWithHeader.label;"/>
|
|
<xul:hbox anonid="search-panel-searchforwith"
|
|
class="search-panel-current-input">
|
|
<xul:label anonid="searchbar-oneoffheader-before"
|
|
value="&searchFor.label;"/>
|
|
<xul:label anonid="searchbar-oneoffheader-searchtext"
|
|
class="search-panel-input-value"
|
|
flex="1"
|
|
crop="end"/>
|
|
<xul:label anonid="searchbar-oneoffheader-after"
|
|
flex="10000"
|
|
value="&searchWith.label;"/>
|
|
</xul:hbox>
|
|
<xul:hbox anonid="search-panel-searchonengine"
|
|
class="search-panel-current-input">
|
|
<xul:label anonid="searchbar-oneoffheader-beforeengine"
|
|
value="&search.label;"/>
|
|
<xul:label anonid="searchbar-oneoffheader-engine"
|
|
class="search-panel-input-value"
|
|
flex="1"
|
|
crop="end"/>
|
|
<xul:label anonid="searchbar-oneoffheader-afterengine"
|
|
flex="10000"
|
|
value="&searchAfter.label;"/>
|
|
</xul:hbox>
|
|
</xul:deck>
|
|
<xul:description anonid="search-panel-one-offs"
|
|
role="group"
|
|
class="search-panel-one-offs"
|
|
xbl:inherits="compact">
|
|
<xul:button anonid="search-settings-compact"
|
|
oncommand="showSettings();"
|
|
class="searchbar-engine-one-off-item search-setting-button-compact"
|
|
tooltiptext="&changeSearchSettings.tooltip;"
|
|
xbl:inherits="compact"/>
|
|
</xul:description>
|
|
<xul:vbox anonid="add-engines" class="search-add-engines"/>
|
|
<xul:button anonid="search-settings"
|
|
oncommand="showSettings();"
|
|
class="search-setting-button search-panel-header"
|
|
label="&changeSearchSettings.button;"
|
|
xbl:inherits="compact"/>
|
|
<xul:menupopup anonid="search-one-offs-context-menu">
|
|
<xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
|
|
label="&searchInNewTab.label;"
|
|
accesskey="&searchInNewTab.accesskey;"/>
|
|
<xul:menuitem anonid="search-one-offs-context-set-default"
|
|
label="&searchSetAsDefault.label;"
|
|
accesskey="&searchSetAsDefault.accesskey;"/>
|
|
</xul:menupopup>
|
|
</content>
|
|
|
|
<implementation implements="nsIDOMEventListener,nsIObserver,nsIWeakReference">
|
|
|
|
<!-- Width in pixels of the one-off buttons. 49px is the min-width of
|
|
each search engine button, adapt this const when changing the css.
|
|
It's actually 48px + 1px of right border. -->
|
|
<property name="buttonWidth" readonly="true" onget="return 49;"/>
|
|
|
|
<field name="_popup">null</field>
|
|
|
|
<!-- The popup that contains the one-offs. This is required, so it should
|
|
never be null or undefined, except possibly before the one-offs are
|
|
used. -->
|
|
<property name="popup">
|
|
<getter><![CDATA[
|
|
return this._popup;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
let events = [
|
|
"popupshowing",
|
|
"popuphidden",
|
|
];
|
|
if (this._popup) {
|
|
for (let event of events) {
|
|
this._popup.removeEventListener(event, this);
|
|
}
|
|
}
|
|
if (val) {
|
|
for (let event of events) {
|
|
val.addEventListener(event, this);
|
|
}
|
|
}
|
|
this._popup = val;
|
|
|
|
// If the popup is already open, rebuild the one-offs now. The
|
|
// popup may be opening, so check that the state is not closed
|
|
// instead of checking popupOpen.
|
|
if (val && val.state != "closed") {
|
|
this._rebuild();
|
|
}
|
|
return val;
|
|
]]></setter>
|
|
</property>
|
|
|
|
<field name="_textbox">null</field>
|
|
<field name="_textboxWidth">0</field>
|
|
|
|
<!-- The textbox associated with the one-offs. Set this to a textbox to
|
|
automatically keep the related one-offs UI up to date. Otherwise you
|
|
can leave it null/undefined, and in that case you should update the
|
|
query property manually. -->
|
|
<property name="textbox">
|
|
<getter><![CDATA[
|
|
return this._textbox;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
if (this._textbox) {
|
|
this._textbox.removeEventListener("input", this);
|
|
}
|
|
if (val) {
|
|
val.addEventListener("input", this);
|
|
}
|
|
return this._textbox = val;
|
|
]]></setter>
|
|
</property>
|
|
|
|
<!-- Set this to a string that identifies your one-offs consumer. It'll
|
|
be appended to telemetry recorded with maybeRecordTelemetry(). -->
|
|
<field name="telemetryOrigin">""</field>
|
|
|
|
<field name="_query">""</field>
|
|
|
|
<!-- The query string currently shown in the one-offs. If the textbox
|
|
property is non-null, then this is automatically updated on
|
|
input. -->
|
|
<property name="query">
|
|
<getter><![CDATA[
|
|
return this._query;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
this._query = val;
|
|
if (this.popup && this.popup.popupOpen) {
|
|
this._updateAfterQueryChanged();
|
|
}
|
|
return val;
|
|
]]></setter>
|
|
</property>
|
|
|
|
<field name="_selectedButton">null</field>
|
|
|
|
<!-- The selected one-off, a xul:button, including the add-engine button
|
|
and the search-settings button. Null if no one-off is selected. -->
|
|
<property name="selectedButton">
|
|
<getter><![CDATA[
|
|
return this._selectedButton;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
if (val && val.classList.contains("dummy")) {
|
|
// Never select dummy buttons.
|
|
val = null;
|
|
}
|
|
if (this._selectedButton) {
|
|
this._selectedButton.removeAttribute("selected");
|
|
}
|
|
if (val) {
|
|
val.setAttribute("selected", "true");
|
|
}
|
|
this._selectedButton = val;
|
|
this._updateStateForButton(null);
|
|
if (val && !val.engine) {
|
|
// If the button doesn't have an engine, then clear the popup's
|
|
// selection to indicate that pressing Return while the button is
|
|
// selected will do the button's command, not search.
|
|
this.popup.selectedIndex = -1;
|
|
}
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("SelectedOneOffButtonChanged", true, false);
|
|
this.dispatchEvent(event);
|
|
return val;
|
|
]]></setter>
|
|
</property>
|
|
|
|
<!-- The index of the selected one-off, including the add-engine button
|
|
and the search-settings button. -1 if no one-off is selected. -->
|
|
<property name="selectedButtonIndex">
|
|
<getter><![CDATA[
|
|
let buttons = this.getSelectableButtons(true);
|
|
for (let i = 0; i < buttons.length; i++) {
|
|
if (buttons[i] == this._selectedButton) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
let buttons = this.getSelectableButtons(true);
|
|
this.selectedButton = buttons[val];
|
|
return val;
|
|
]]></setter>
|
|
</property>
|
|
|
|
<property name="compact" readonly="true">
|
|
<getter><![CDATA[
|
|
return this.getAttribute("compact") == "true";
|
|
]]></getter>
|
|
</property>
|
|
|
|
<field name="buttons" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs");
|
|
</field>
|
|
<field name="header" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header");
|
|
</field>
|
|
<field name="addEngines" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
|
|
</field>
|
|
<field name="settingsButton" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
|
|
</field>
|
|
<field name="settingsButtonCompact" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
|
|
</field>
|
|
|
|
<field name="_bundle">null</field>
|
|
|
|
<property name="bundle" readonly="true">
|
|
<getter><![CDATA[
|
|
if (!this._bundle) {
|
|
const kBundleURI = "chrome://browser/locale/search.properties";
|
|
this._bundle = Services.strings.createBundle(kBundleURI);
|
|
}
|
|
return this._bundle;
|
|
]]></getter>
|
|
</property>
|
|
|
|
<!-- When a context menu is opened on a one-off button, this is set to the
|
|
engine of that button for use with the context menu actions. -->
|
|
<field name="_contextEngine">null</field>
|
|
|
|
<constructor><![CDATA[
|
|
// Prevent popup events from the context menu from reaching the autocomplete
|
|
// binding (or other listeners).
|
|
let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
|
|
let listener = aEvent => aEvent.stopPropagation();
|
|
menu.addEventListener("popupshowing", listener);
|
|
menu.addEventListener("popuphiding", listener);
|
|
menu.addEventListener("popupshown", aEvent => {
|
|
this._ignoreMouseEvents = true;
|
|
aEvent.stopPropagation();
|
|
});
|
|
menu.addEventListener("popuphidden", aEvent => {
|
|
this._ignoreMouseEvents = false;
|
|
aEvent.stopPropagation();
|
|
});
|
|
|
|
// Add weak referenced observers to invalidate our cached list of engines.
|
|
Services.prefs.addObserver("browser.search.hiddenOneOffs", this, true);
|
|
Services.obs.addObserver(this, "browser-search-engine-modified", true);
|
|
|
|
// Rebuild the buttons when the theme changes. See bug 1357800 for
|
|
// details. Summary: On Linux, switching between themes can cause a row
|
|
// of buttons to disappear.
|
|
Services.obs.addObserver(this, "lightweight-theme-changed", true);
|
|
]]></constructor>
|
|
|
|
<!-- This handles events outside the one-off buttons, like on the popup
|
|
and textbox. -->
|
|
<method name="handleEvent">
|
|
<parameter name="event"/>
|
|
<body><![CDATA[
|
|
switch (event.type) {
|
|
case "input":
|
|
// Allow the consumer's input to override its value property with
|
|
// a oneOffSearchQuery property. That way if the value is not
|
|
// actually what the user typed (e.g., it's autofilled, or it's a
|
|
// mozaction URI), the consumer has some way of providing it.
|
|
this.query = event.target.oneOffSearchQuery || event.target.value;
|
|
break;
|
|
case "popupshowing":
|
|
this._rebuild();
|
|
break;
|
|
case "popuphidden":
|
|
Services.tm.dispatchToMainThread(() => {
|
|
this.selectedButton = null;
|
|
this._contextEngine = null;
|
|
});
|
|
break;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="observe">
|
|
<parameter name="aEngine"/>
|
|
<parameter name="aTopic"/>
|
|
<parameter name="aData"/>
|
|
<body><![CDATA[
|
|
// Make sure the engine list is refetched next time it's needed.
|
|
this._engines = null;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="showSettings">
|
|
<body><![CDATA[
|
|
BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin);
|
|
|
|
openPreferences("paneSearch", {origin: "contentSearch"});
|
|
|
|
// If the preference tab was already selected, the panel doesn't
|
|
// close itself automatically.
|
|
this.popup.hidePopup();
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- Updates the parts of the UI that show the query string. -->
|
|
<method name="_updateAfterQueryChanged">
|
|
<body><![CDATA[
|
|
let headerSearchText =
|
|
document.getAnonymousElementByAttribute(this, "anonid",
|
|
"searchbar-oneoffheader-searchtext");
|
|
headerSearchText.setAttribute("value", this.query);
|
|
let groupText;
|
|
let isOneOffSelected =
|
|
this.selectedButton &&
|
|
this.selectedButton.classList.contains("searchbar-engine-one-off-item");
|
|
// Typing de-selects the settings or opensearch buttons at the bottom
|
|
// of the search panel, as typing shows the user intends to search.
|
|
if (this.selectedButton && !isOneOffSelected)
|
|
this.selectedButton = null;
|
|
if (this.query) {
|
|
groupText = headerSearchText.previousSibling.value +
|
|
'"' + headerSearchText.value + '"' +
|
|
headerSearchText.nextSibling.value;
|
|
if (!isOneOffSelected)
|
|
this.header.selectedIndex = 1;
|
|
} else {
|
|
let noSearchHeader =
|
|
document.getAnonymousElementByAttribute(this, "anonid",
|
|
"searchbar-oneoffheader-search");
|
|
groupText = noSearchHeader.value;
|
|
if (!isOneOffSelected)
|
|
this.header.selectedIndex = 0;
|
|
}
|
|
this.buttons.setAttribute("aria-label", groupText);
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_engines">null</field>
|
|
<property name="engines" readonly="true">
|
|
<getter><![CDATA[
|
|
if (this._engines)
|
|
return this._engines;
|
|
let currentEngineNameToIgnore;
|
|
if (!this.getAttribute("includecurrentengine"))
|
|
currentEngineNameToIgnore = Services.search.currentEngine.name;
|
|
|
|
let pref = Services.prefs.getStringPref("browser.search.hiddenOneOffs");
|
|
let hiddenList = pref ? pref.split(",") : [];
|
|
|
|
this._engines = Services.search.getVisibleEngines().filter(e => {
|
|
let name = e.name;
|
|
return (!currentEngineNameToIgnore ||
|
|
name != currentEngineNameToIgnore) &&
|
|
!hiddenList.includes(name);
|
|
});
|
|
|
|
return this._engines;
|
|
]]></getter>
|
|
</property>
|
|
|
|
<!-- Builds all the UI. -->
|
|
<method name="_rebuild">
|
|
<body><![CDATA[
|
|
// Update the 'Search for <keywords> with:" header.
|
|
this._updateAfterQueryChanged();
|
|
|
|
// Handle opensearch items. This needs to be done before building the
|
|
// list of one off providers, as that code will return early if all the
|
|
// alternative engines are hidden.
|
|
// Skip this in compact mode, ie. for the urlbar.
|
|
if (!this.compact)
|
|
this._rebuildAddEngineList();
|
|
|
|
// Check if the one-off buttons really need to be rebuilt.
|
|
if (this._textbox) {
|
|
// We can't get a reliable value for the popup width without flushing,
|
|
// but the popup width won't change if the textbox width doesn't.
|
|
let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
let textboxWidth =
|
|
DOMUtils.getBoundsWithoutFlushing(this._textbox).width;
|
|
// We can return early if neither the list of engines nor the panel
|
|
// width has changed.
|
|
if (this._engines && this._textboxWidth == textboxWidth) {
|
|
return;
|
|
}
|
|
this._textboxWidth = textboxWidth;
|
|
}
|
|
|
|
// Finally, build the list of one-off buttons.
|
|
while (this.buttons.firstChild != this.settingsButtonCompact)
|
|
this.buttons.firstChild.remove();
|
|
// Remove the trailing empty text node introduced by the binding's
|
|
// content markup above.
|
|
if (this.settingsButtonCompact.nextSibling)
|
|
this.settingsButtonCompact.nextSibling.remove();
|
|
|
|
let engines = this.engines;
|
|
let oneOffCount = engines.length;
|
|
|
|
// header is a xul:deck so collapsed doesn't work on it, see bug 589569.
|
|
this.header.hidden = this.buttons.collapsed = !oneOffCount;
|
|
|
|
if (!oneOffCount)
|
|
return;
|
|
|
|
let panelWidth = parseInt(this.popup.clientWidth);
|
|
|
|
// There's one weird thing to guard against: when layout pixels
|
|
// aren't an integral multiple of device pixels, the last button
|
|
// of each row sometimes gets pushed to the next row, depending on the
|
|
// panel and button widths.
|
|
// This is likely because the clientWidth getter rounds the value, but
|
|
// the panel's border width is not an integer.
|
|
// As a workaround, decrement the width if the scale is not an integer.
|
|
let scale = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils)
|
|
.screenPixelsPerCSSPixel;
|
|
if (Math.floor(scale) != scale) {
|
|
--panelWidth;
|
|
}
|
|
|
|
// The + 1 is because the last button doesn't have a right border.
|
|
let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
|
|
let buttonWidth = Math.floor(panelWidth / enginesPerRow);
|
|
// There will be an emtpy area of:
|
|
// panelWidth - enginesPerRow * buttonWidth px
|
|
// at the end of each row.
|
|
|
|
// If the <description> tag with the list of search engines doesn't have
|
|
// a fixed height, the panel will be sized incorrectly, causing the bottom
|
|
// of the suggestion <tree> to be hidden.
|
|
if (this.compact)
|
|
++oneOffCount;
|
|
let rowCount = Math.ceil(oneOffCount / enginesPerRow);
|
|
let height = rowCount * 33; // 32px per row, 1px border.
|
|
this.buttons.setAttribute("height", height + "px");
|
|
|
|
// Ensure we can refer to the settings buttons by ID:
|
|
let origin = this.telemetryOrigin;
|
|
this.settingsButton.id = origin + "-anon-search-settings";
|
|
this.settingsButtonCompact.id = origin + "-anon-search-settings-compact";
|
|
|
|
const kXULNS =
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow);
|
|
for (let i = 0; i < engines.length; ++i) {
|
|
let engine = engines[i];
|
|
let button = document.createElementNS(kXULNS, "button");
|
|
button.id = this._buttonIDForEngine(engine);
|
|
let uri = "chrome://browser/skin/search-engine-placeholder.png";
|
|
if (engine.iconURI) {
|
|
uri = engine.iconURI.spec;
|
|
}
|
|
button.setAttribute("image", uri);
|
|
button.setAttribute("class", "searchbar-engine-one-off-item");
|
|
button.setAttribute("tooltiptext", engine.name);
|
|
button.setAttribute("width", buttonWidth);
|
|
button.engine = engine;
|
|
|
|
if ((i + 1) % enginesPerRow == 0)
|
|
button.classList.add("last-of-row");
|
|
|
|
if (i + 1 == engines.length)
|
|
button.classList.add("last-engine");
|
|
|
|
if (i >= oneOffCount + dummyItems - enginesPerRow)
|
|
button.classList.add("last-row");
|
|
|
|
this.buttons.insertBefore(button, this.settingsButtonCompact);
|
|
}
|
|
|
|
let hasDummyItems = !!dummyItems;
|
|
while (dummyItems) {
|
|
let button = document.createElementNS(kXULNS, "button");
|
|
button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
|
|
button.setAttribute("width", buttonWidth);
|
|
|
|
if (!--dummyItems)
|
|
button.classList.add("last-of-row");
|
|
|
|
this.buttons.insertBefore(button, this.settingsButtonCompact);
|
|
}
|
|
|
|
if (this.compact) {
|
|
this.settingsButtonCompact.setAttribute("width", buttonWidth);
|
|
if (rowCount == 1 && hasDummyItems) {
|
|
// When there's only one row, make the compact settings button
|
|
// hug the right edge of the panel. It may not due to the panel's
|
|
// width not being an integral multiple of the button width. (See
|
|
// the "There will be an emtpy area" comment above.) Increase the
|
|
// width of the last dummy item by the remainder.
|
|
let remainder = panelWidth - (enginesPerRow * buttonWidth);
|
|
let width = remainder + buttonWidth;
|
|
let lastDummyItem = this.settingsButtonCompact.previousSibling;
|
|
lastDummyItem.setAttribute("width", width);
|
|
}
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- If a page offers more than this number of engines, the add-engines
|
|
menu button is shown, instead of showing the engines directly in the
|
|
popup. -->
|
|
<field name="_addEngineMenuThreshold">5</field>
|
|
|
|
<method name="_rebuildAddEngineList">
|
|
<body><![CDATA[
|
|
let list = this.addEngines;
|
|
while (list.firstChild) {
|
|
list.firstChild.remove();
|
|
}
|
|
|
|
// Add a button for each engine that the page in the selected browser
|
|
// offers, except when there are too many offered engines.
|
|
// The popup isn't designed to handle too many (by scrolling for
|
|
// example), so a page could break the popup by offering too many.
|
|
// Instead, add a single menu button with a submenu of all the engines.
|
|
|
|
if (!gBrowser.selectedBrowser.engines) {
|
|
return;
|
|
}
|
|
|
|
const kXULNS =
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
let engines = gBrowser.selectedBrowser.engines;
|
|
let tooManyEngines = engines.length > this._addEngineMenuThreshold;
|
|
|
|
if (tooManyEngines) {
|
|
// Make the top-level menu button.
|
|
let button = document.createElementNS(kXULNS, "button");
|
|
list.appendChild(button);
|
|
button.classList.add("addengine-item");
|
|
button.setAttribute("anonid", "addengine-menu-button");
|
|
button.setAttribute("type", "menu");
|
|
button.setAttribute("label",
|
|
this.bundle.GetStringFromName("cmd_addFoundEngineMenu"));
|
|
button.setAttribute("crop", "end");
|
|
button.setAttribute("pack", "start");
|
|
|
|
// Set the menu button's image to the image of the first engine. The
|
|
// offered engines may have differing images, so there's no perfect
|
|
// choice here.
|
|
let engine = engines[0];
|
|
if (engine.icon) {
|
|
button.setAttribute("image", engine.icon);
|
|
}
|
|
|
|
// Now make the button's child menupopup.
|
|
list = document.createElementNS(kXULNS, "menupopup");
|
|
button.appendChild(list);
|
|
list.setAttribute("anonid", "addengine-menu");
|
|
list.setAttribute("position", "topright topleft");
|
|
|
|
// Events from child menupopups bubble up to the autocomplete binding,
|
|
// which breaks it, so prevent these events from propagating.
|
|
let suppressEventTypes = [
|
|
"popupshowing",
|
|
"popuphiding",
|
|
"popupshown",
|
|
"popuphidden",
|
|
];
|
|
for (let type of suppressEventTypes) {
|
|
list.addEventListener(type, event => {
|
|
event.stopPropagation();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Finally, add the engines to the list. If there aren't too many
|
|
// engines, the list is the add-engines vbox. Otherwise it's the
|
|
// menupopup created earlier. In the latter case, create menuitem
|
|
// elements instead of buttons, because buttons don't get keyboard
|
|
// handling for free inside menupopups.
|
|
let eltType = tooManyEngines ? "menuitem" : "button";
|
|
for (let engine of engines) {
|
|
let button = document.createElementNS(kXULNS, eltType);
|
|
button.classList.add("addengine-item");
|
|
button.id = this.telemetryOrigin + "-add-engine-" +
|
|
this._fixUpEngineNameForID(engine.title);
|
|
let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
|
|
[engine.title], 1);
|
|
button.setAttribute("label", label);
|
|
button.setAttribute("crop", "end");
|
|
button.setAttribute("tooltiptext", engine.title + "\n" + engine.uri);
|
|
button.setAttribute("uri", engine.uri);
|
|
button.setAttribute("title", engine.title);
|
|
if (engine.icon) {
|
|
button.setAttribute("image", engine.icon);
|
|
}
|
|
if (tooManyEngines) {
|
|
button.classList.add("menuitem-iconic");
|
|
} else {
|
|
button.setAttribute("pack", "start");
|
|
}
|
|
list.appendChild(button);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_buttonIDForEngine">
|
|
<parameter name="engine"/>
|
|
<body><![CDATA[
|
|
return this.telemetryOrigin + "-engine-one-off-item-" +
|
|
this._fixUpEngineNameForID(engine.name);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_fixUpEngineNameForID">
|
|
<parameter name="name"/>
|
|
<body><![CDATA[
|
|
return name.replace(/ /g, "-");
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_buttonForEngine">
|
|
<parameter name="engine"/>
|
|
<body><![CDATA[
|
|
return document.getElementById(this._buttonIDForEngine(engine));
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
Updates the popup and textbox for the currently selected or moused-over
|
|
button.
|
|
|
|
@param mousedOverButton
|
|
The currently moused-over button, or null if there isn't one.
|
|
-->
|
|
<method name="_updateStateForButton">
|
|
<parameter name="mousedOverButton"/>
|
|
<body><![CDATA[
|
|
let button = mousedOverButton;
|
|
|
|
// Ignore dummy buttons.
|
|
if (button && button.classList.contains("dummy")) {
|
|
button = null;
|
|
}
|
|
|
|
// If there's no moused-over button, then the one-offs should reflect
|
|
// the selected button, if any.
|
|
button = button || this.selectedButton;
|
|
|
|
if (!button) {
|
|
this.header.selectedIndex = this.query ? 1 : 0;
|
|
if (this.textbox) {
|
|
this.textbox.removeAttribute("aria-activedescendant");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (button.classList.contains("searchbar-engine-one-off-item") &&
|
|
button.engine) {
|
|
let headerEngineText =
|
|
document.getAnonymousElementByAttribute(this, "anonid",
|
|
"searchbar-oneoffheader-engine");
|
|
this.header.selectedIndex = 2;
|
|
headerEngineText.value = button.engine.name;
|
|
} else {
|
|
this.header.selectedIndex = this.query ? 1 : 0;
|
|
}
|
|
if (this.textbox) {
|
|
this.textbox.setAttribute("aria-activedescendant", button.id);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="getSelectableButtons">
|
|
<parameter name="aIncludeNonEngineButtons"/>
|
|
<body><![CDATA[
|
|
let buttons = [];
|
|
for (let oneOff = this.buttons.firstChild; oneOff; oneOff = oneOff.nextSibling) {
|
|
// oneOff may be a text node since the list xul:description contains
|
|
// whitespace and the compact settings button. See the markup
|
|
// above. _rebuild removes text nodes, but it may not have been
|
|
// called yet (because e.g. the popup hasn't been opened yet).
|
|
if (oneOff.nodeType == Node.ELEMENT_NODE) {
|
|
if (oneOff.classList.contains("dummy") ||
|
|
oneOff.classList.contains("search-setting-button-compact"))
|
|
break;
|
|
buttons.push(oneOff);
|
|
}
|
|
}
|
|
|
|
if (aIncludeNonEngineButtons) {
|
|
for (let addEngine = this.addEngines.firstChild; addEngine; addEngine = addEngine.nextSibling) {
|
|
buttons.push(addEngine);
|
|
}
|
|
buttons.push(this.compact ? this.settingsButtonCompact : this.settingsButton);
|
|
}
|
|
|
|
return buttons;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="handleSearchCommand">
|
|
<parameter name="aEvent"/>
|
|
<parameter name="aEngine"/>
|
|
<parameter name="aForceNewTab"/>
|
|
<body><![CDATA[
|
|
let where = "current";
|
|
let params;
|
|
|
|
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
|
|
if (aForceNewTab) {
|
|
where = "tab";
|
|
if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
|
|
params = {
|
|
inBackground: true,
|
|
};
|
|
}
|
|
} else {
|
|
var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
|
|
if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
|
|
where = "tab";
|
|
if ((aEvent instanceof MouseEvent) &&
|
|
(aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
|
|
where = "tab";
|
|
params = {
|
|
inBackground: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
this.popup.handleOneOffSearch(aEvent, aEngine, where, params);
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
Increments or decrements the index of the currently selected one-off.
|
|
|
|
@param aForward
|
|
If true, the index is incremented, and if false, the index is
|
|
decremented.
|
|
@param aIncludeNonEngineButtons
|
|
If true, non-dummy buttons that do not have engines are included.
|
|
These buttons include the OpenSearch and settings buttons. For
|
|
example, if the currently selected button is an engine button,
|
|
the next button is the settings button, and you pass true for
|
|
aForward, then passing true for this value would cause the
|
|
settings to be selected. Passing false for this value would
|
|
cause the selection to clear or wrap around, depending on what
|
|
value you passed for the aWrapAround parameter.
|
|
@param aWrapAround
|
|
If true, the selection wraps around between the first and last
|
|
buttons.
|
|
@return True if the selection can continue to advance after this method
|
|
returns and false if not.
|
|
-->
|
|
<method name="advanceSelection">
|
|
<parameter name="aForward"/>
|
|
<parameter name="aIncludeNonEngineButtons"/>
|
|
<parameter name="aWrapAround"/>
|
|
<body><![CDATA[
|
|
let buttons = this.getSelectableButtons(aIncludeNonEngineButtons);
|
|
let index;
|
|
if (this.selectedButton) {
|
|
let inc = aForward ? 1 : -1;
|
|
let oldIndex = buttons.indexOf(this.selectedButton);
|
|
index = ((oldIndex + inc) + buttons.length) % buttons.length;
|
|
if (!aWrapAround &&
|
|
((aForward && index <= oldIndex) ||
|
|
(!aForward && oldIndex <= index))) {
|
|
// The index has wrapped around, but wrapping around isn't
|
|
// allowed.
|
|
index = -1;
|
|
}
|
|
} else {
|
|
index = aForward ? 0 : buttons.length - 1;
|
|
}
|
|
this.selectedButton = index < 0 ? null : buttons[index];
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
This handles key presses specific to the one-off buttons like Tab and
|
|
Alt+Up/Down, and Up/Down keys within the buttons. Since one-off buttons
|
|
are always used in conjunction with a list of some sort (in this.popup),
|
|
it also handles Up/Down keys that cross the boundaries between list
|
|
items and the one-off buttons.
|
|
|
|
If this method handles the key press, then event.defaultPrevented will
|
|
be true when it returns.
|
|
|
|
@param event
|
|
The key event.
|
|
@param numListItems
|
|
The number of items in the list. The reason that this is a
|
|
parameter at all is that the list may contain items at the end
|
|
that should be ignored, depending on the consumer. That's true
|
|
for the urlbar for example.
|
|
@param allowEmptySelection
|
|
Pass true if it's OK that neither the list nor the one-off
|
|
buttons contains a selection. Pass false if either the list or
|
|
the one-off buttons (or both) should always contain a selection.
|
|
@param textboxUserValue
|
|
When the last list item is selected and the user presses Down,
|
|
the first one-off becomes selected and the textbox value is
|
|
restored to the value that the user typed. Pass that value here.
|
|
However, if you pass true for allowEmptySelection, you don't need
|
|
to pass anything for this parameter. (Pass undefined or null.)
|
|
-->
|
|
<method name="handleKeyPress">
|
|
<parameter name="event"/>
|
|
<parameter name="numListItems"/>
|
|
<parameter name="allowEmptySelection"/>
|
|
<parameter name="textboxUserValue"/>
|
|
<body><![CDATA[
|
|
if (!this.popup) {
|
|
return;
|
|
}
|
|
let handled = this._handleKeyPress(event, numListItems,
|
|
allowEmptySelection,
|
|
textboxUserValue);
|
|
if (handled) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_handleKeyPress">
|
|
<parameter name="event"/>
|
|
<parameter name="numListItems"/>
|
|
<parameter name="allowEmptySelection"/>
|
|
<parameter name="textboxUserValue"/>
|
|
<body><![CDATA[
|
|
if (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
|
|
this.selectedButton &&
|
|
this.selectedButton.getAttribute("anonid") ==
|
|
"addengine-menu-button") {
|
|
// If the add-engine overflow menu item is selected and the user
|
|
// presses the right arrow key, open the submenu. Unfortunately
|
|
// handling the left arrow key -- to close the popup -- isn't
|
|
// straightforward. Once the popup is open, it consumes all key
|
|
// events. Setting ignorekeys=handled on it doesn't help, since the
|
|
// popup handles all arrow keys. Setting ignorekeys=true on it does
|
|
// mean that the popup no longer consumes the left arrow key, but
|
|
// then it no longer handles up/down keys to select items in the
|
|
// popup.
|
|
this.selectedButton.open = true;
|
|
return true;
|
|
}
|
|
|
|
// Handle the Tab key, but only if non-Shift modifiers aren't also
|
|
// pressed to avoid clobbering other shortcuts (like the Alt+Tab
|
|
// browser tab switcher). The reason this uses getModifierState() and
|
|
// checks for "AltGraph" is that when you press Shift-Alt-Tab,
|
|
// event.altKey is actually false for some reason, at least on macOS.
|
|
// getModifierState("Alt") is also false, but "AltGraph" is true.
|
|
if (event.keyCode == KeyEvent.DOM_VK_TAB &&
|
|
!event.getModifierState("Alt") &&
|
|
!event.getModifierState("AltGraph") &&
|
|
!event.getModifierState("Control") &&
|
|
!event.getModifierState("Meta")) {
|
|
if (this.getAttribute("disabletab") == "true" ||
|
|
(event.shiftKey &&
|
|
this.selectedButtonIndex <= 0) ||
|
|
(!event.shiftKey &&
|
|
this.selectedButtonIndex ==
|
|
this.getSelectableButtons(true).length - 1)) {
|
|
this.selectedButton = null;
|
|
return false;
|
|
}
|
|
this.popup.selectedIndex = -1;
|
|
this.advanceSelection(!event.shiftKey, true, false);
|
|
return !!this.selectedButton;
|
|
}
|
|
|
|
if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP) {
|
|
if (event.altKey) {
|
|
// Keep the currently selected result in the list (if any) as a
|
|
// secondary "alt" selection and move the selection up within the
|
|
// buttons.
|
|
this.advanceSelection(false, false, false);
|
|
return true;
|
|
}
|
|
if (numListItems == 0) {
|
|
this.advanceSelection(false, true, false);
|
|
return true;
|
|
}
|
|
if (this.popup.selectedIndex > 0) {
|
|
// Moving up within the list. The autocomplete controller should
|
|
// handle this case. A button may be selected, so null it.
|
|
this.selectedButton = null;
|
|
return false;
|
|
}
|
|
if (this.popup.selectedIndex == 0) {
|
|
// Moving up from the top of the list.
|
|
if (allowEmptySelection) {
|
|
// Let the autocomplete controller remove selection in the list
|
|
// and revert the typed text in the textbox.
|
|
return false;
|
|
}
|
|
// Wrap selection around to the last button.
|
|
if (this.textbox && typeof(textboxUserValue) == "string") {
|
|
this.textbox.value = textboxUserValue;
|
|
}
|
|
this.advanceSelection(false, true, true);
|
|
return true;
|
|
}
|
|
if (!this.selectedButton) {
|
|
// Moving up from no selection in the list or the buttons, back
|
|
// down to the last button.
|
|
this.advanceSelection(false, true, true);
|
|
return true;
|
|
}
|
|
if (this.selectedButtonIndex == 0) {
|
|
// Moving up from the buttons to the bottom of the list.
|
|
this.selectedButton = null;
|
|
return false;
|
|
}
|
|
// Moving up/left within the buttons.
|
|
this.advanceSelection(false, true, false);
|
|
return true;
|
|
}
|
|
|
|
if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
|
|
if (event.altKey) {
|
|
// Keep the currently selected result in the list (if any) as a
|
|
// secondary "alt" selection and move the selection down within
|
|
// the buttons.
|
|
this.advanceSelection(true, false, false);
|
|
return true;
|
|
}
|
|
if (numListItems == 0) {
|
|
this.advanceSelection(true, true, false);
|
|
return true;
|
|
}
|
|
if (this.popup.selectedIndex >= 0 &&
|
|
this.popup.selectedIndex < numListItems - 1) {
|
|
// Moving down within the list. The autocomplete controller
|
|
// should handle this case. A button may be selected, so null it.
|
|
this.selectedButton = null;
|
|
return false;
|
|
}
|
|
if (this.popup.selectedIndex == numListItems - 1) {
|
|
// Moving down from the last item in the list to the buttons.
|
|
this.selectedButtonIndex = 0;
|
|
if (allowEmptySelection) {
|
|
// Let the autocomplete controller remove selection in the list
|
|
// and revert the typed text in the textbox.
|
|
return false;
|
|
}
|
|
if (this.textbox && typeof(textboxUserValue) == "string") {
|
|
this.textbox.value = textboxUserValue;
|
|
}
|
|
this.popup.selectedIndex = -1;
|
|
return true;
|
|
}
|
|
if (this.selectedButton) {
|
|
let buttons = this.getSelectableButtons(true);
|
|
if (this.selectedButtonIndex == buttons.length - 1) {
|
|
// Moving down from the buttons back up to the top of the list.
|
|
this.selectedButton = null;
|
|
if (allowEmptySelection) {
|
|
// Prevent the selection from wrapping around to the top of
|
|
// the list by returning true, since the list currently has no
|
|
// selection. Nothing should be selected after handling this
|
|
// Down key.
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
// Moving down/right within the buttons.
|
|
this.advanceSelection(true, true, false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_LEFT) {
|
|
if (this.selectedButton &&
|
|
(this.compact || this.selectedButton.engine)) {
|
|
// Moving left within the buttons.
|
|
this.advanceSelection(false, this.compact, true);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
|
|
if (this.selectedButton &&
|
|
(this.compact || this.selectedButton.engine)) {
|
|
// Moving right within the buttons.
|
|
this.advanceSelection(true, this.compact, true);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
If the given event is related to the one-offs, this method records
|
|
one-off telemetry for it. this.telemetryOrigin will be appended to the
|
|
computed source, so make sure you set that first.
|
|
|
|
@param aEvent
|
|
An event, like a click on a one-off button.
|
|
@param aOpenUILinkWhere
|
|
The "where" passed to openUILink.
|
|
@param aOpenUILinkParams
|
|
The "params" passed to openUILink.
|
|
@return True if telemetry was recorded and false if not.
|
|
-->
|
|
<method name="maybeRecordTelemetry">
|
|
<parameter name="aEvent"/>
|
|
<parameter name="aOpenUILinkWhere"/>
|
|
<parameter name="aOpenUILinkParams"/>
|
|
<body><![CDATA[
|
|
if (!aEvent) {
|
|
return false;
|
|
}
|
|
|
|
let source = null;
|
|
let type = "unknown";
|
|
let engine = null;
|
|
let target = aEvent.originalTarget;
|
|
|
|
if (aEvent instanceof KeyboardEvent) {
|
|
type = "key";
|
|
if (this.selectedButton) {
|
|
source = "oneoff";
|
|
engine = this.selectedButton.engine;
|
|
}
|
|
} else if (aEvent instanceof MouseEvent) {
|
|
type = "mouse";
|
|
if (target.classList.contains("searchbar-engine-one-off-item")) {
|
|
source = "oneoff";
|
|
engine = target.engine;
|
|
}
|
|
} else if ((aEvent instanceof XULCommandEvent) &&
|
|
target.getAttribute("anonid") ==
|
|
"search-one-offs-context-open-in-new-tab") {
|
|
source = "oneoff-context";
|
|
engine = this._contextEngine;
|
|
}
|
|
|
|
if (!source) {
|
|
return false;
|
|
}
|
|
|
|
if (this.telemetryOrigin) {
|
|
source += "-" + this.telemetryOrigin;
|
|
}
|
|
|
|
let tabBackground = aOpenUILinkWhere == "tab" &&
|
|
aOpenUILinkParams &&
|
|
aOpenUILinkParams.inBackground;
|
|
let where = tabBackground ? "tab-background" : aOpenUILinkWhere;
|
|
BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type,
|
|
where);
|
|
return true;
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- All this stuff is to make the add-engines menu button behave like an
|
|
actual menu. The add-engines menu button is shown when there are
|
|
many engines offered by the current site. -->
|
|
<field name="_addEngineMenuTimeoutMs">200</field>
|
|
<field name="_addEngineMenuTimeout">null</field>
|
|
<field name="_addEngineMenuShouldBeOpen">false</field>
|
|
|
|
<method name="_resetAddEngineMenuTimeout">
|
|
<body><![CDATA[
|
|
if (this._addEngineMenuTimeout) {
|
|
clearTimeout(this._addEngineMenuTimeout);
|
|
}
|
|
this._addEngineMenuTimeout = setTimeout(() => {
|
|
delete this._addEngineMenuTimeout;
|
|
let button = document.getAnonymousElementByAttribute(
|
|
this, "anonid", "addengine-menu-button"
|
|
);
|
|
button.open = this._addEngineMenuShouldBeOpen;
|
|
}, this._addEngineMenuTimeoutMs);
|
|
]]></body>
|
|
</method>
|
|
|
|
</implementation>
|
|
|
|
<handlers>
|
|
|
|
<handler event="mousedown"><![CDATA[
|
|
let target = event.originalTarget;
|
|
if (target.getAttribute("anonid") == "addengine-menu-button") {
|
|
return;
|
|
}
|
|
// Required to receive click events from the buttons on Linux.
|
|
event.preventDefault();
|
|
]]></handler>
|
|
|
|
<handler event="mousemove"><![CDATA[
|
|
let target = event.originalTarget;
|
|
|
|
// Handle mouseover on the add-engine menu button and its popup items.
|
|
if (target.getAttribute("anonid") == "addengine-menu-button" ||
|
|
(target.localName == "menuitem" &&
|
|
target.classList.contains("addengine-item"))) {
|
|
let menuButton = document.getAnonymousElementByAttribute(
|
|
this, "anonid", "addengine-menu-button"
|
|
);
|
|
this._updateStateForButton(menuButton);
|
|
this._addEngineMenuShouldBeOpen = true;
|
|
this._resetAddEngineMenuTimeout();
|
|
return;
|
|
}
|
|
|
|
if (target.localName != "button")
|
|
return;
|
|
|
|
// Ignore mouse events when the context menu is open.
|
|
if (this._ignoreMouseEvents)
|
|
return;
|
|
|
|
let isOneOff =
|
|
target.classList.contains("searchbar-engine-one-off-item") &&
|
|
!target.classList.contains("dummy");
|
|
if (isOneOff ||
|
|
target.classList.contains("addengine-item") ||
|
|
target.classList.contains("search-setting-button")) {
|
|
this._updateStateForButton(target);
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="mouseout"><![CDATA[
|
|
|
|
let target = event.originalTarget;
|
|
|
|
// Handle mouseout on the add-engine menu button and its popup items.
|
|
if (target.getAttribute("anonid") == "addengine-menu-button" ||
|
|
(target.localName == "menuitem" &&
|
|
target.classList.contains("addengine-item"))) {
|
|
this._updateStateForButton(null);
|
|
this._addEngineMenuShouldBeOpen = false;
|
|
this._resetAddEngineMenuTimeout();
|
|
return;
|
|
}
|
|
|
|
if (target.localName != "button") {
|
|
return;
|
|
}
|
|
|
|
// Don't update the mouseover state if the context menu is open.
|
|
if (this._ignoreMouseEvents)
|
|
return;
|
|
|
|
this._updateStateForButton(null);
|
|
]]></handler>
|
|
|
|
<handler event="click"><![CDATA[
|
|
if (event.button == 2)
|
|
return; // ignore right clicks.
|
|
|
|
let button = event.originalTarget;
|
|
let engine = button.engine;
|
|
|
|
if (!engine)
|
|
return;
|
|
|
|
// Select the clicked button so that consumers can easily tell which
|
|
// button was acted on.
|
|
this.selectedButton = button;
|
|
this.handleSearchCommand(event, engine);
|
|
]]></handler>
|
|
|
|
<handler event="command"><![CDATA[
|
|
let target = event.originalTarget;
|
|
if (target.classList.contains("addengine-item")) {
|
|
// On success, hide the panel and tell event listeners to reshow it to
|
|
// show the new engine.
|
|
let installCallback = {
|
|
onSuccess: engine => {
|
|
this._rebuild();
|
|
},
|
|
onError(errorCode) {
|
|
if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
|
|
// Download error is shown by the search service
|
|
return;
|
|
}
|
|
const kSearchBundleURI = "chrome://global/locale/search/search.properties";
|
|
let searchBundle = Services.strings.createBundle(kSearchBundleURI);
|
|
let brandBundle = document.getElementById("bundle_brand");
|
|
let brandName = brandBundle.getString("brandShortName");
|
|
let title = searchBundle.GetStringFromName("error_invalid_engine_title");
|
|
let text = searchBundle.formatStringFromName("error_duplicate_engine_msg",
|
|
[brandName, target.getAttribute("uri")], 2);
|
|
Services.prompt.QueryInterface(Ci.nsIPromptFactory);
|
|
let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
|
|
prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
|
|
prompt.setPropertyAsBool("allowTabModal", true);
|
|
prompt.alert(title, text);
|
|
}
|
|
};
|
|
Services.search.addEngine(target.getAttribute("uri"), null,
|
|
target.getAttribute("image"), false,
|
|
installCallback);
|
|
}
|
|
let anonid = target.getAttribute("anonid");
|
|
if (anonid == "search-one-offs-context-open-in-new-tab") {
|
|
// Select the context-clicked button so that consumers can easily
|
|
// tell which button was acted on.
|
|
this.selectedButton = this._buttonForEngine(this._contextEngine);
|
|
this.handleSearchCommand(event, this._contextEngine, true);
|
|
}
|
|
if (anonid == "search-one-offs-context-set-default") {
|
|
let currentEngine = Services.search.currentEngine;
|
|
|
|
if (!this.getAttribute("includecurrentengine")) {
|
|
// Make the target button of the context menu reflect the current
|
|
// search engine first. Doing this as opposed to rebuilding all the
|
|
// one-off buttons avoids flicker.
|
|
let button = this._buttonForEngine(this._contextEngine);
|
|
button.id = this._buttonIDForEngine(currentEngine);
|
|
let uri = "chrome://browser/skin/search-engine-placeholder.png";
|
|
if (currentEngine.iconURI)
|
|
uri = currentEngine.iconURI.spec;
|
|
button.setAttribute("image", uri);
|
|
button.setAttribute("tooltiptext", currentEngine.name);
|
|
button.engine = currentEngine;
|
|
}
|
|
|
|
Services.search.currentEngine = this._contextEngine;
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="contextmenu"><![CDATA[
|
|
let target = event.originalTarget;
|
|
// Prevent the context menu from appearing except on the one off buttons.
|
|
if (!target.classList.contains("searchbar-engine-one-off-item") ||
|
|
target.classList.contains("dummy")) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
|
|
.setAttribute("disabled", target.engine == Services.search.currentEngine);
|
|
|
|
this._contextEngine = target.engine;
|
|
]]></handler>
|
|
</handlers>
|
|
|
|
</binding>
|
|
|
|
</bindings>
|