gecko-dev/browser/base/content/urlbarBindings.xml
Harry Twyford 93f19f1f25 Bug 1460097 - Autocompleted URLs are trimmed. r=mak
MozReview-Commit-ID: 6y26a9Nhm3b

--HG--
extra : rebase_source : 7b56edf0185f4d15e989d6f71b259eae16c4e099
2018-05-16 14:12:16 -04:00

2685 lines
105 KiB
XML

<?xml version="1.0"?>
<!--
-*- Mode: HTML -*-
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/.
-->
<!DOCTYPE bindings [
<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
%notificationDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
]>
<bindings id="urlbarBindings" xmlns="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
<content sizetopopup="pref">
<xul:hbox flex="1" class="urlbar-textbox-container">
<children includes="image|deck|stack|box"/>
<xul:hbox anonid="textbox-input-box"
class="textbox-input-box urlbar-input-box"
flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
<children/>
<html:input anonid="input"
class="autocomplete-textbox urlbar-input textbox-input"
allowevents="true"
inputmode="mozAwesomebar"
xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,focused,textoverflow"/>
</xul:hbox>
<xul:image anonid="urlbar-go-button"
class="urlbar-go-button urlbar-icon"
onclick="gURLBar.handleCommand(event);"
tooltiptext="&goEndCap.tooltip;"
xbl:inherits="pageproxystate,parentfocused=focused,usertyping"/>
<xul:dropmarker anonid="historydropmarker"
class="urlbar-history-dropmarker urlbar-icon chromeclass-toolbar-additional"
tooltiptext="&urlbar.openHistoryPopup.tooltip;"
allowevents="true"
xbl:inherits="open,parentfocused=focused,usertyping"/>
<children includes="hbox"/>
</xul:hbox>
<xul:popupset anonid="popupset"
class="autocomplete-result-popupset"/>
<children includes="toolbarbutton"/>
</content>
<implementation implements="nsIObserver">
<field name="ExtensionSearchHandler" readonly="true">
(ChromeUtils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
</field>
<constructor><![CDATA[
this._prefs = Cc["@mozilla.org/preferences-service;1"]
.getService(Ci.nsIPrefService)
.getBranch("browser.urlbar.");
this._prefs.addObserver("", this);
this._defaultPrefs = Cc["@mozilla.org/preferences-service;1"]
.getService(Ci.nsIPrefService)
.getDefaultBranch("browser.urlbar.");
Services.prefs.addObserver("browser.search.suggest.enabled", this);
this.browserSearchSuggestEnabled = Services.prefs.getBoolPref("browser.search.suggest.enabled");
this.openInTab = this._prefs.getBoolPref("openintab");
this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
this.speculativeConnectEnabled = this._prefs.getBoolPref("speculativeConnect.enabled");
this.urlbarSearchSuggestEnabled = this._prefs.getBoolPref("suggest.searches");
this.timeout = this._prefs.getIntPref("delay");
this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled");
this._mayTrimURLs = this._prefs.getBoolPref("trimURLs");
this._adoptIntoActiveWindow = this._prefs.getBoolPref("switchTabs.adoptIntoActiveWindow");
this.inputField.controllers.insertControllerAt(0, this._copyCutController);
this.inputField.addEventListener("paste", this);
this.inputField.addEventListener("mousedown", this);
this.inputField.addEventListener("mousemove", this);
this.inputField.addEventListener("mouseout", this);
this.inputField.addEventListener("overflow", this);
this.inputField.addEventListener("underflow", this);
var textBox = document.getAnonymousElementByAttribute(this,
"anonid", "textbox-input-box");
var cxmenu = document.getAnonymousElementByAttribute(textBox,
"anonid", "input-box-contextmenu");
var pasteAndGo;
cxmenu.addEventListener("popupshowing", function() {
if (!pasteAndGo)
return;
var controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
var enabled = controller.isCommandEnabled("cmd_paste");
if (enabled)
pasteAndGo.removeAttribute("disabled");
else
pasteAndGo.setAttribute("disabled", "true");
});
var insertLocation = cxmenu.firstChild;
while (insertLocation.nextSibling &&
insertLocation.getAttribute("cmd") != "cmd_paste")
insertLocation = insertLocation.nextSibling;
if (insertLocation) {
pasteAndGo = document.createElement("menuitem");
let label = Services.strings.createBundle("chrome://browser/locale/browser.properties").
GetStringFromName("pasteAndGo.label");
pasteAndGo.setAttribute("label", label);
pasteAndGo.setAttribute("anonid", "paste-and-go");
pasteAndGo.setAttribute("oncommand",
"gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();");
cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling);
}
this.popup.addEventListener("popupshowing", () => {
this._enableOrDisableOneOffSearches();
}, {capturing: true, once: true});
// history dropmarker open state
this.popup.addEventListener("popupshowing", () => {
this.setAttribute("open", "true");
});
this.popup.addEventListener("popuphidden", () => {
requestAnimationFrame(() => {
this.removeAttribute("open");
});
});
]]></constructor>
<destructor><![CDATA[
// Somehow, it's possible for the XBL destructor to fire without the
// constructor ever having fired. Fix:
if (!this._prefs) {
return;
}
this._prefs.removeObserver("", this);
this._prefs = null;
Services.prefs.removeObserver("browser.search.suggest.enabled", this);
this.inputField.controllers.removeController(this._copyCutController);
this.inputField.removeEventListener("paste", this);
this.inputField.removeEventListener("mousedown", this);
this.inputField.removeEventListener("mousemove", this);
this.inputField.removeEventListener("mouseout", this);
this.inputField.removeEventListener("overflow", this);
this.inputField.removeEventListener("underflow", this);
if (this._deferredKeyEventTimeout) {
clearTimeout(this._deferredKeyEventTimeout);
this._deferredKeyEventTimeout = null;
}
// Null out the one-offs' popup and textbox so that it cleans up its
// internal state for both. Most importantly, it removes the event
// listeners that it added to both.
this.popup.oneOffSearchButtons.popup = null;
this.popup.oneOffSearchButtons.textbox = null;
]]></destructor>
<field name="goButton">
document.getAnonymousElementByAttribute(this, "anonid", "urlbar-go-button");
</field>
<field name="_value">""</field>
<field name="gotResultForCurrentQuery">false</field>
<!--
This is set around HandleHenter so it can be used in handleCommand.
It is also used to track whether we must handle a delayed handleEnter,
by checking if it has been cleared.
-->
<field name="handleEnterInstance">null</field>
<!--
Since we never want scrollbars, we always use the maxResults value.
-->
<property name="maxRows"
onget="return this.popup.maxResults;"/>
<!--
Set by focusAndSelectUrlBar to indicate whether the next focus event was
initiated by an explicit user action. See the "focus" handler below.
-->
<field name="userInitiatedFocus">false</field>
<!--
onBeforeValueGet is called by the base-binding's .value getter.
It can return an object with a "value" property, to override the
return value of the getter.
-->
<method name="onBeforeValueGet">
<body><![CDATA[
return { value: this._value };
]]></body>
</method>
<!--
onBeforeValueSet is called by the base-binding's .value setter.
It should return the value that the setter should use.
-->
<method name="onBeforeValueSet">
<parameter name="aValue"/>
<body><![CDATA[
this._value = aValue;
var returnValue = aValue;
var action = this._parseActionUrl(aValue);
if (action) {
switch (action.type) {
case "switchtab": // Fall through.
case "remotetab": // Fall through.
case "visiturl": {
returnValue = action.params.displayUrl;
break;
}
case "keyword": // Fall through.
case "searchengine": {
returnValue = action.params.input;
break;
}
case "extension": {
returnValue = action.params.content;
break;
}
}
} else {
let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(aValue);
if (originalUrl) {
returnValue = originalUrl.displaySpec;
}
}
// Set the actiontype only if the user is not overriding actions.
if (action && this._pressedNoActionKeys.size == 0) {
this.setAttribute("actiontype", action.type);
} else {
this.removeAttribute("actiontype");
}
return returnValue;
]]></body>
</method>
<method name="onKeyPress">
<parameter name="aEvent"/>
<parameter name="aNoDefer"/>
<body><![CDATA[
switch (aEvent.keyCode) {
case KeyEvent.DOM_VK_LEFT:
case KeyEvent.DOM_VK_RIGHT:
case KeyEvent.DOM_VK_HOME:
// Reset the selected index so that nsAutoCompleteController
// simply closes the popup without trying to fill anything.
this.popup.selectedIndex = -1;
break;
case KeyEvent.DOM_VK_TAB:
this.userSelectionBehavior = "tab";
break;
case KeyEvent.DOM_VK_UP:
case KeyEvent.DOM_VK_DOWN:
case KeyEvent.DOM_VK_PAGE_UP:
case KeyEvent.DOM_VK_PAGE_DOWN:
if (this.userSelectionBehavior != "tab")
this.userSelectionBehavior = "arrow";
break;
}
if (!this.popup.disableKeyNavigation) {
if (!aNoDefer && this._shouldDeferKeyEvent(aEvent)) {
this._deferKeyEvent(aEvent, "onKeyPress");
return false;
}
if (this.popup.popupOpen && this.popup.handleKeyPress(aEvent)) {
return true;
}
}
return this.handleKeyPress(aEvent);
]]></body>
</method>
<!--
Search results arrive asynchronously, which means that keypresses may
arrive before results do and therefore not have the effect the user
intends. That's especially likely to happen with the down arrow and
enter keys due to the one-off search buttons: if the user very quickly
pastes something in the input, presses the down arrow key, and then hits
enter, they are probably expecting to visit the first result. But if
there are no results, then pressing down and enter will trigger the
first one-off button. To prevent that undesirable behavior, certain
keys are buffered and deferred until more results arrive, at which time
they're replayed.
@param event
The key event that should maybe be deferred.
@return True if the event should be deferred, false if not.
-->
<method name="_shouldDeferKeyEvent">
<parameter name="event"/>
<body><![CDATA[
// If any event has been deferred for this search, then defer all
// subsequent events so that the user does not experience any
// keypresses out of order. All events will be replayed when
// _deferredKeyEventTimeout fires.
if (this._deferredKeyEventQueue.length) {
return true;
}
// At this point, no events have been deferred for this search, and we
// need to decide whether `event` is the first one that should be.
if (!this._keyCodesToDefer.has(event.keyCode)) {
// Not a key that should trigger deferring.
return false;
}
let waitedLongEnough =
this._searchStartDate + this._deferredKeyEventTimeoutMs <= Cu.now();
if (waitedLongEnough) {
// This is a key that we would defer, but enough time has passed
// since the start of the search that we don't want to block the
// user's keypresses anymore.
return false;
}
if (event.keyCode == KeyEvent.DOM_VK_TAB && !this.popupOpen) {
// The popup is closed and the user pressed the Tab key. The
// focus should move out of the urlbar immediately.
return false;
}
return !this._safeToPlayDeferredKeyEvent(event);
]]></body>
</method>
<!--
Returns true if the given deferred key event can be played now without
possibly surprising the user. This depends on the state of the popup,
its results, and the type of keypress. Use this method only after
determining that the event should be deferred, or after it's already
been deferred and you want to know if it can be played now.
@param event
The key event.
@return True if the event can be played, false if not.
-->
<method name="_safeToPlayDeferredKeyEvent">
<parameter name="event"/>
<body><![CDATA[
if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
return this.popup.selectedIndex != 0 ||
this.gotResultForCurrentQuery;
}
if (!this.gotResultForCurrentQuery || !this.popupOpen) {
// We're still waiting on the first result, or the popup hasn't
// opened yet, so not safe.
return false;
}
let maxResultsRemaining =
this.popup.maxResults - this.popup.matchCount;
if (maxResultsRemaining == 0) {
// The popup can't possibly have any more results, so there's no
// need to defer any event now.
return true;
}
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
// Don't play the event if the last result is selected so that the
// user doesn't accidentally arrow down into the one-off buttons
// when they didn't mean to.
let lastResultSelected =
this.popup.selectedIndex + 1 == this.popup.matchCount;
return !lastResultSelected;
}
return true;
]]></body>
</method>
<!--
Adds a key event to the deferred event queue.
@param event
The key event to defer.
@param methodName
The name of the method on `this` to call. It's expected to take
two arguments: the event, and a noDefer bool. If the bool is
true, then the event is being replayed and it should not be
deferred.
-->
<method name="_deferKeyEvent">
<parameter name="event"/>
<parameter name="methodName"/>
<body><![CDATA[
// Somehow event.defaultPrevented ends up true for deferred events.
// autocomplete ignores defaultPrevented events, which means it would
// ignore replayed deferred events if we didn't tell it to bypass
// defaultPrevented. That's the purpose of this expando. If we could
// figure out what's setting defaultPrevented and prevent it, then we
// could get rid of this.
if (event.urlbarDeferred) {
throw new Error("Key event already deferred!");
}
event.urlbarDeferred = true;
this._deferredKeyEventQueue.push({
methodName,
event,
searchString: this.mController.searchString,
});
if (!this._deferredKeyEventTimeout) {
// Start the timeout that will unconditionally replay all deferred
// events when it fires so that, after a certain point, we don't
// keep blocking the user's keypresses when nothing else has caused
// the events to be replayed. Do not check whether it's safe to
// replay the events because otherwise it may look like we ignored
// the user's input.
let elapsed = Cu.now() - this._searchStartDate;
let remaining = this._deferredKeyEventTimeoutMs - elapsed;
this._deferredKeyEventTimeout = setTimeout(() => {
this.replayAllDeferredKeyEvents();
this._deferredKeyEventTimeout = null;
}, Math.max(0, remaining));
}
]]></body>
</method>
<!-- The enter key is always deferred, so it's not included here. -->
<field name="_keyCodesToDefer">new Set([
KeyboardEvent.DOM_VK_RETURN,
KeyboardEvent.DOM_VK_DOWN,
KeyboardEvent.DOM_VK_TAB,
])</field>
<field name="_deferredKeyEventQueue">[]</field>
<field name="_deferredKeyEventTimeout">null</field>
<field name="_deferredKeyEventTimeoutMs">200</field>
<field name="_searchStartDate">0</field>
<method name="replaySafeDeferredKeyEvents">
<body><![CDATA[
if (!this._deferredKeyEventQueue.length) {
return;
}
let instance = this._deferredKeyEventQueue[0];
if (!this._safeToPlayDeferredKeyEvent(instance.event)) {
return;
}
this._deferredKeyEventQueue.shift();
this._replayKeyEventInstance(instance);
Services.tm.dispatchToMainThread(() => {
this.replaySafeDeferredKeyEvents();
});
]]></body>
</method>
<!--
Unconditionally replays all deferred key events. This does not check
whether it's safe to replay the events; use replaySafeDeferredKeyEvents
for that. Use this method when you must replay all events so that it
does not appear that we ignored the user's input.
-->
<method name="replayAllDeferredKeyEvents">
<body><![CDATA[
let instance = this._deferredKeyEventQueue.shift();
if (!instance) {
return;
}
this._replayKeyEventInstance(instance);
Services.tm.dispatchToMainThread(() => {
this.replayAllDeferredKeyEvents();
});
]]></body>
</method>
<method name="_replayKeyEventInstance">
<parameter name="instance"/>
<body><![CDATA[
// Safety check: handle only if the search string didn't change.
if (this.mController.searchString == instance.searchString) {
this[instance.methodName](instance.event, true);
}
]]></body>
</method>
<field name="_mayTrimURLs">true</field>
<method name="trimValue">
<parameter name="aURL"/>
<body><![CDATA[
// This method must not modify the given URL such that calling
// nsIURIFixup::createFixupURI with the result will produce a different URI.
return this._mayTrimURLs ? trimURL(aURL) : aURL;
]]></body>
</method>
<field name="_formattingEnabled">true</field>
<method name="formatValue">
<body><![CDATA[
if (!this._formattingEnabled || !this.editor)
return;
let controller = this.editor.selectionController;
let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
strikeOut.removeAllRanges();
let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
selection.removeAllRanges();
if (this.focused)
return;
let textNode = this.editor.rootElement.firstChild;
let value = textNode.textContent;
if (!value)
return;
// Get the URL from the fixup service:
let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
let uriInfo;
try {
uriInfo = Services.uriFixup.getFixupURIInfo(value, flags);
} catch (ex) {}
// Ignore if we couldn't make a URI out of this, the URI resulted in a search,
// or the URI has a non-http(s)/ftp protocol.
if (!uriInfo ||
!uriInfo.fixedURI ||
uriInfo.keywordProviderName ||
!["http", "https", "ftp"].includes(uriInfo.fixedURI.scheme)) {
return;
}
// If we trimmed off the http scheme, ensure we stick it back on before
// trying to figure out what domain we're accessing, so we don't get
// confused by user:pass@host http URLs. We later use
// trimmedLength to ensure we don't count the length of a trimmed protocol
// when determining which parts of the URL to highlight as "preDomain".
let trimmedLength = 0;
if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) {
value = "http://" + value;
trimmedLength = "http://".length;
}
let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/);
if (!matchedURL)
return;
// Strike out the "https" part if mixed active content is loaded.
if (this.getAttribute("pageproxystate") == "valid" &&
value.startsWith("https:") &&
gBrowser.securityUI.state &
Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
let range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, 5);
strikeOut.addRange(range);
}
let [, preDomain, domain] = matchedURL;
let baseDomain = domain;
let subDomain = "";
try {
baseDomain = Services.eTLD.getBaseDomainFromHost(uriInfo.fixedURI.host);
if (!domain.endsWith(baseDomain)) {
// getBaseDomainFromHost converts its resultant to ACE.
let IDNService = Cc["@mozilla.org/network/idn-service;1"]
.getService(Ci.nsIIDNService);
baseDomain = IDNService.convertACEtoUTF8(baseDomain);
}
} catch (e) {}
if (baseDomain != domain) {
subDomain = domain.slice(0, -baseDomain.length);
}
let rangeLength = preDomain.length + subDomain.length - trimmedLength;
if (rangeLength) {
let range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, rangeLength);
selection.addRange(range);
}
let startRest = preDomain.length + domain.length - trimmedLength;
if (startRest < value.length - trimmedLength) {
let range = document.createRange();
range.setStart(textNode, startRest);
range.setEnd(textNode, value.length - trimmedLength);
selection.addRange(range);
}
]]></body>
</method>
<method name="handleRevert">
<body><![CDATA[
var isScrolling = this.popupOpen;
gBrowser.userTypedValue = null;
// don't revert to last valid url unless page is NOT loading
// and user is NOT key-scrolling through autocomplete list
if (!XULBrowserWindow.isBusy && !isScrolling) {
URLBarSetURI();
// If the value isn't empty and the urlbar has focus, select the value.
if (this.value && this.hasAttribute("focused"))
this.select();
}
// tell widget to revert to last typed text only if the user
// was scrolling when they hit escape
return !isScrolling;
]]></body>
</method>
<method name="_whereToOpen">
<parameter name="event"/>
<body><![CDATA[
let isMouseEvent = event instanceof MouseEvent;
let reuseEmpty = !isMouseEvent;
let where = undefined;
if (isMouseEvent) {
where = whereToOpenLink(event, false, false);
} else {
let altEnter = event && event.altKey;
where = altEnter ? "tab" : "current";
}
if (this.openInTab) {
if (where == "current") {
where = "tab";
} else if (where == "tab") {
where = "current";
}
reuseEmpty = true;
}
if (where == "tab" && reuseEmpty && isTabEmpty(gBrowser.selectedTab)) {
where = "current";
}
return where;
]]></body>
</method>
<!--
This is ultimately called by the autocomplete controller as the result
of handleEnter when the Return key is pressed in the textbox. Since
onPopupClick also calls handleEnter, this is also called as a result in
that case.
@param event
The event that triggered the command.
@param openUILinkWhere
Optional. The "where" to pass to openTrustedLinkIn. This method
computes the appropriate "where" given the event, but you can
use this to override it.
@param openUILinkParams
Optional. The parameters to pass to openTrustedLinkIn. As with
"where", this method computes the appropriate parameters, but
any parameters you supply here will override those.
-->
<method name="handleCommand">
<parameter name="event"/>
<parameter name="openUILinkWhere"/>
<parameter name="openUILinkParams"/>
<parameter name="triggeringPrincipal"/>
<body><![CDATA[
let isMouseEvent = event instanceof MouseEvent;
if (isMouseEvent && event.button == 2) {
// Do nothing for right clicks.
return;
}
BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
event, this.userSelectionBehavior);
// Determine whether to use the selected one-off search button. In
// one-off search buttons parlance, "selected" means that the button
// has been navigated to via the keyboard. So we want to use it if
// the triggering event is not a mouse click -- i.e., it's a Return
// key -- or if the one-off was mouse-clicked.
let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton;
if (selectedOneOff &&
isMouseEvent &&
event.originalTarget != selectedOneOff) {
selectedOneOff = null;
}
// Do the command of the selected one-off if it's not an engine.
if (selectedOneOff && !selectedOneOff.engine) {
selectedOneOff.doCommand();
return;
}
let where = openUILinkWhere || this._whereToOpen(event);
let url = this.value;
if (!url) {
return;
}
let mayInheritPrincipal = false;
let postData = null;
let browser = gBrowser.selectedBrowser;
let action = this._parseActionUrl(url);
if (selectedOneOff && selectedOneOff.engine) {
// If there's a selected one-off button then load a search using
// the one-off's engine.
[url, postData] =
this._parseAndRecordSearchEngineLoad(selectedOneOff.engine,
this.oneOffSearchQuery,
event, where,
openUILinkParams);
} else if (action) {
switch (action.type) {
case "visiturl":
// Unifiedcomplete uses fixupURI to tell if something is a visit
// or a search, and passes out the fixedURI as the url param.
// By using that uri we would end up passing a different string
// to the docshell that may run a different not-found heuristic.
// For example, "mozilla/run" would be fixed by unifiedcomplete
// to "http://mozilla/run". The docshell, once it can't resolve
// mozilla, would note the string has a scheme, and try to load
// http://mozilla.com/run instead of searching "mozilla/run".
// So, if we have the original input at hand, we pass it through
// and let the docshell handle it.
if (action.params.input) {
url = action.params.input;
break;
}
url = action.params.url;
break;
case "remotetab":
url = action.params.url;
break;
case "keyword":
if (action.params.postData) {
postData = getPostDataStream(action.params.postData);
}
mayInheritPrincipal = true;
url = action.params.url;
break;
case "switchtab":
url = action.params.url;
if (this.hasAttribute("actiontype")) {
this.handleRevert();
let prevTab = gBrowser.selectedTab;
let loadOpts = {
adoptIntoActiveWindow: this._adoptIntoActiveWindow,
};
if (switchToTabHavingURI(url, false, loadOpts) &&
isTabEmpty(prevTab)) {
gBrowser.removeTab(prevTab);
}
return;
}
break;
case "searchengine":
if (selectedOneOff && selectedOneOff.engine) {
// Replace the engine with the selected one-off engine.
action.params.engineName = selectedOneOff.engine.name;
}
const actionDetails = {
isSuggestion: !!action.params.searchSuggestion,
isAlias: !!action.params.alias
};
[url, postData] = this._parseAndRecordSearchEngineLoad(
action.params.engineName,
action.params.searchSuggestion || action.params.searchQuery,
event,
where,
openUILinkParams,
actionDetails
);
break;
case "extension":
this.handleRevert();
// Give the extension control of handling the command.
let searchString = action.params.content;
let keyword = action.params.keyword;
this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
return;
}
} else {
// This is a fallback for add-ons and old testing code that directly
// set value and try to confirm it. UnifiedComplete should always
// resolve to a valid url.
try {
url = url.trim();
new URL(url);
} catch (ex) {
let lastLocationChange = browser.lastLocationChange;
getShortcutOrURIAndPostData(url).then(data => {
if (where != "current" ||
browser.lastLocationChange == lastLocationChange) {
this._loadURL(data.url, browser, data.postData, where,
openUILinkParams, data.mayInheritPrincipal,
triggeringPrincipal);
}
});
return;
}
}
this._loadURL(url, browser, postData, where, openUILinkParams,
mayInheritPrincipal, triggeringPrincipal);
]]></body>
</method>
<property name="oneOffSearchQuery">
<getter><![CDATA[
// If the user has selected a search suggestion, chances are they
// want to use the one off search engine to search for that suggestion,
// not the string that they manually entered into the location bar.
let action = this._parseActionUrl(this.value);
if (action && action.type == "searchengine") {
return action.params.input;
}
// this.textValue may be an autofilled string. Search only with the
// portion that the user typed, if any, by preferring the autocomplete
// controller's searchString (including handleEnterInstance.searchString).
return this.handleEnterSearchString ||
this.mController.searchString ||
this.textValue;
]]></getter>
</property>
<method name="_loadURL">
<parameter name="url"/>
<parameter name="browser"/>
<parameter name="postData"/>
<parameter name="openUILinkWhere"/>
<parameter name="openUILinkParams"/>
<parameter name="mayInheritPrincipal"/>
<parameter name="triggeringPrincipal"/>
<body><![CDATA[
this.value = url;
browser.userTypedValue = url;
if (gInitialPages.includes(url)) {
browser.initialPageLoadedFromURLBar = url;
}
try {
addToUrlbarHistory(url);
} catch (ex) {
// Things may go wrong when adding url to session history,
// but don't let that interfere with the loading of the url.
Cu.reportError(ex);
}
let params = {
postData,
allowThirdPartyFixup: true,
triggeringPrincipal,
};
if (openUILinkWhere == "current") {
params.targetBrowser = browser;
params.indicateErrorPageLoad = true;
params.allowPinnedTabHostChange = true;
params.disallowInheritPrincipal = !mayInheritPrincipal;
params.allowPopups = url.startsWith("javascript:");
} else {
params.initiatingDoc = document;
}
if (openUILinkParams) {
for (let key in openUILinkParams) {
params[key] = openUILinkParams[key];
}
}
// Focus the content area before triggering loads, since if the load
// occurs in a new tab, we want focus to be restored to the content
// area when the current tab is re-selected.
browser.focus();
if (openUILinkWhere != "current") {
this.handleRevert();
}
try {
openTrustedLinkIn(url, openUILinkWhere, params);
} catch (ex) {
// This load can throw an exception in certain cases, which means
// we'll want to replace the URL with the loaded URL:
if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
this.handleRevert();
}
}
if (openUILinkWhere == "current") {
// Ensure the start of the URL is visible for usability reasons.
this.selectionStart = this.selectionEnd = 0;
}
]]></body>
</method>
<method name="_parseAndRecordSearchEngineLoad">
<parameter name="engineOrEngineName"/>
<parameter name="query"/>
<parameter name="event"/>
<parameter name="openUILinkWhere"/>
<parameter name="openUILinkParams"/>
<parameter name="searchActionDetails"/>
<body><![CDATA[
let engine =
typeof(engineOrEngineName) == "string" ?
Services.search.getEngineByName(engineOrEngineName) :
engineOrEngineName;
let isOneOff = this.popup.oneOffSearchButtons
.maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
// Infer the type of the event which triggered the search.
let eventType = "unknown";
if (event instanceof KeyboardEvent) {
eventType = "key";
} else if (event instanceof MouseEvent) {
eventType = "mouse";
}
// Augment the search action details object.
let details = searchActionDetails || {};
details.isOneOff = isOneOff;
details.type = eventType;
BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
let submission = engine.getSubmission(query, null, "keyword");
return [submission.uri.spec, submission.postData];
]]></body>
</method>
<method name="maybeCanonizeURL">
<parameter name="aTriggeringEvent"/>
<parameter name="aUrl"/>
<body><![CDATA[
// Only add the suffix when the URL bar value isn't already "URL-like",
// and only if we get a keyboard event, to match user expectations.
if (!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl) ||
!(aTriggeringEvent instanceof KeyboardEvent)) {
return;
}
let url = aUrl;
let accel = AppConstants.platform == "macosx" ?
aTriggeringEvent.metaKey :
aTriggeringEvent.ctrlKey;
let shift = aTriggeringEvent.shiftKey;
let suffix = "";
switch (true) {
case (accel && shift):
suffix = ".org/";
break;
case (shift):
suffix = ".net/";
break;
case (accel):
try {
suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
if (suffix.charAt(suffix.length - 1) != "/")
suffix += "/";
} catch (e) {
suffix = ".com/";
}
break;
}
if (!suffix)
return;
// trim leading/trailing spaces (bug 233205)
url = url.trim();
// Tack www. and suffix on. If user has appended directories, insert
// suffix before them (bug 279035). Be careful not to get two slashes.
let firstSlash = url.indexOf("/");
if (firstSlash >= 0) {
url = url.substring(0, firstSlash) + suffix +
url.substring(firstSlash + 1);
} else {
url = url + suffix;
}
this.popup.overrideValue = "http://www." + url;
]]></body>
</method>
<method name="_initURLTooltip">
<body><![CDATA[
if (this.focused || !this.hasAttribute("textoverflow"))
return;
this.inputField.setAttribute("tooltiptext", this.value);
]]></body>
</method>
<method name="_hideURLTooltip">
<body><![CDATA[
this.inputField.removeAttribute("tooltiptext");
]]></body>
</method>
<!-- Returns:
null if there's a security issue and we should do nothing.
a URL object if there is one that we're OK with loading,
a text value otherwise.
-->
<method name="_getDroppableItem">
<parameter name="aEvent"/>
<body><![CDATA[
let links;
try {
links = browserDragAndDrop.dropLinks(aEvent);
} catch (ex) {
// this is possibly a security exception, in which case we should return
// null. Always return null because we can't *know* what exception is
// being returned.
return null;
}
// The URL bar automatically handles inputs with newline characters,
// so we can get away with treating text/x-moz-url flavours as text/plain.
if (links.length > 0 && links[0].url) {
let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent);
aEvent.preventDefault();
let url = links[0].url;
let strippedURL = stripUnsafeProtocolOnPaste(url);
if (strippedURL != url) {
aEvent.stopImmediatePropagation();
return null;
}
let urlObj;
try {
// If this throws, urlSecurityCheck would also throw, as that's what it
// does with things that don't pass the IO service's newURI constructor
// without fixup. It's conceivable we may want to relax this check in
// the future (so e.g. www.foo.com gets fixed up), but not right now.
urlObj = new URL(url);
// If we succeed, try to pass security checks. If this works, return the
// URL object. If the *security checks* fail, return null.
try {
urlSecurityCheck(url,
triggeringPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
return urlObj;
} catch (ex) {
return null;
}
} catch (ex) {
// We couldn't make a URL out of this. Continue on, and return text below.
}
}
return aEvent.dataTransfer.getData("text/unicode");
]]></body>
</method>
<method name="onDragOver">
<parameter name="aEvent"/>
<body><![CDATA[
if (!this._getDroppableItem(aEvent)) {
aEvent.dataTransfer.dropEffect = "none";
}
]]></body>
</method>
<method name="onDrop">
<parameter name="aEvent"/>
<body><![CDATA[
let droppedItem = this._getDroppableItem(aEvent);
if (droppedItem) {
let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent);
this.value = droppedItem instanceof URL ? droppedItem.href : droppedItem;
SetPageProxyState("invalid");
this.focus();
this.handleCommand(null, undefined, undefined, triggeringPrincipal);
// Force not showing the dropped URI immediately.
gBrowser.userTypedValue = null;
URLBarSetURI();
}
]]></body>
</method>
<method name="makeURIReadable">
<parameter name="aURI"/>
<body>
<![CDATA[
// Avoid copying 'about:reader?url=', and always provide the original URI:
// Reader mode ensures we call createExposableURI itself.
let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(aURI.displaySpec);
if (readerStrippedURI) {
aURI = readerStrippedURI;
} else {
// Only copy exposable URIs
try {
aURI = Services.uriFixup.createExposableURI(aURI);
} catch (ex) {}
}
return aURI;
]]>
</body>
</method>
<method name="_getSelectedValueForClipboard">
<body><![CDATA[
// Grab the actual input field's value, not our value, which could
// include "moz-action:".
var inputVal = this.inputField.value;
let selection = this.editor.selection;
const flags = Ci.nsIDocumentEncoder.OutputPreformatted |
Ci.nsIDocumentEncoder.OutputRaw;
let selectedVal = selection.toStringWithFormat("text/plain", flags, 0);
// Handle multiple-range selection as a string for simplicity.
if (selection.rangeCount > 1) {
return selectedVal;
}
// If the selection doesn't start at the beginning or doesn't span the
// full domain or the URL bar is modified or there is no text at all,
// nothing else to do here.
if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "")
return selectedVal;
// The selection doesn't span the full domain if it doesn't contain a slash and is
// followed by some character other than a slash.
if (!selectedVal.includes("/")) {
let remainder = inputVal.replace(selectedVal, "");
if (remainder != "" && remainder[0] != "/")
return selectedVal;
}
// If the value was filled by a search suggestion, just return it.
let action = this._parseActionUrl(this.value);
if (action && action.type == "searchengine")
return selectedVal;
let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
let uri;
if (this.getAttribute("pageproxystate") == "valid") {
uri = gBrowser.currentURI;
} else {
// We're dealing with an autocompleted value, create a new URI from that.
try {
uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
} catch (e) {}
if (!uri)
return selectedVal;
}
uri = this.makeURIReadable(uri);
// If the entire URL is selected, just use the actual loaded URI,
// unless we want a decoded URI, or it's a data: or javascript: URI,
// since those are hard to read when encoded.
if (inputVal == selectedVal &&
!uri.schemeIs("javascript") && !uri.schemeIs("data") &&
!Services.prefs.getBoolPref("browser.urlbar.decodeURLsOnCopy")) {
return uri.displaySpec;
}
// Just the beginning of the URL is selected, or we want a decoded
// url. First check for a trimmed value.
let spec = uri.displaySpec;
let trimmedSpec = this.trimValue(spec);
if (spec != trimmedSpec) {
// Prepend the portion that trimValue removed from the beginning.
// This assumes trimValue will only truncate the URL at
// the beginning or end (or both).
let trimmedSegments = spec.split(trimmedSpec);
selectedVal = trimmedSegments[0] + selectedVal;
}
return selectedVal;
]]></body>
</method>
<field name="_copyCutController"><![CDATA[
({
urlbar: this,
doCommand(aCommand) {
var urlbar = this.urlbar;
var val = urlbar._getSelectedValueForClipboard();
if (!val)
return;
if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) {
let start = urlbar.selectionStart;
let end = urlbar.selectionEnd;
urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
urlbar.inputField.value.substring(end);
urlbar.selectionStart = urlbar.selectionEnd = start;
let event = document.createEvent("UIEvents");
event.initUIEvent("input", true, false, window, 0);
urlbar.dispatchEvent(event);
SetPageProxyState("invalid");
}
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(val);
},
supportsCommand(aCommand) {
switch (aCommand) {
case "cmd_copy":
case "cmd_cut":
return true;
}
return false;
},
isCommandEnabled(aCommand) {
return this.supportsCommand(aCommand) &&
(aCommand != "cmd_cut" || !this.urlbar.readOnly) &&
this.urlbar.selectionStart < this.urlbar.selectionEnd;
},
onEvent(aEventName) {}
})
]]></field>
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body><![CDATA[
if (aTopic == "nsPref:changed") {
switch (aData) {
case "clickSelectsAll":
case "doubleClickSelectsAll":
this[aData] = this._prefs.getBoolPref(aData);
break;
case "autoFill":
this.completeDefaultIndex = this._prefs.getBoolPref(aData);
break;
case "delay":
this.timeout = this._prefs.getIntPref(aData);
break;
case "formatting.enabled":
this._formattingEnabled = this._prefs.getBoolPref(aData);
break;
case "speculativeConnect.enabled":
this.speculativeConnectEnabled = this._prefs.getBoolPref(aData);
break;
case "openintab":
this.openInTab = this._prefs.getBoolPref(aData);
break;
case "browser.search.suggest.enabled":
this.browserSearchSuggestEnabled = Services.prefs.getBoolPref(aData);
break;
case "suggest.searches":
this.urlbarSearchSuggestEnabled = this._prefs.getBoolPref(aData);
case "userMadeSearchSuggestionsChoice":
// Mirror the value for future use, see the comment in the
// binding's constructor.
this._prefs.setBoolPref("searchSuggestionsChoice",
this.urlbarSearchSuggestEnabled);
// Clear the cached value to allow changing conditions in tests.
delete this._whichSearchSuggestionsNotification;
break;
case "trimURLs":
this._mayTrimURLs = this._prefs.getBoolPref(aData);
break;
case "oneOffSearches":
this._enableOrDisableOneOffSearches();
break;
case "maxRichResults":
this.popup.maxResults = this._prefs.getIntPref(aData);
break;
case "switchTabs.adoptIntoActiveWindow":
this._adoptIntoActiveWindow =
this._prefs.getBoolPref("switchTabs.adoptIntoActiveWindow");
break;
}
}
]]></body>
</method>
<method name="_enableOrDisableOneOffSearches">
<body><![CDATA[
let enable = this._prefs.getBoolPref("oneOffSearches");
this.popup.enableOneOffSearches(enable);
]]></body>
</method>
<method name="handleEvent">
<parameter name="aEvent"/>
<body><![CDATA[
switch (aEvent.type) {
case "paste":
let originalPasteData = aEvent.clipboardData.getData("text/plain");
if (!originalPasteData) {
return;
}
let oldValue = this.inputField.value;
let oldStart = oldValue.substring(0, this.inputField.selectionStart);
// If there is already non-whitespace content in the URL bar
// preceding the pasted content, it's not necessary to check
// protocols used by the pasted content:
if (oldStart.trim()) {
return;
}
let oldEnd = oldValue.substring(this.inputField.selectionEnd);
let pasteData = stripUnsafeProtocolOnPaste(originalPasteData);
if (originalPasteData != pasteData) {
// Unfortunately we're not allowed to set the bits being pasted
// so cancel this event:
aEvent.preventDefault();
aEvent.stopImmediatePropagation();
this.inputField.value = oldStart + pasteData + oldEnd;
// Fix up cursor/selection:
let newCursorPos = oldStart.length + pasteData.length;
this.inputField.selectionStart = newCursorPos;
this.inputField.selectionEnd = newCursorPos;
}
break;
case "mousedown":
if (this.doubleClickSelectsAll &&
aEvent.button == 0 && aEvent.detail == 2) {
this.editor.selectAll();
aEvent.preventDefault();
}
break;
case "mousemove":
this._initURLTooltip();
break;
case "mouseout":
this._hideURLTooltip();
break;
case "overflow":
if (!this.value) {
// We initially get a spurious overflow event from the
// anonymous div containing the placeholder text; bail out.
break;
}
this.setAttribute("textoverflow", "true");
break;
case "underflow":
this.removeAttribute("textoverflow");
this._hideURLTooltip();
break;
case "TabSelect":
// The autocomplete controller uses heuristic on some internal caches
// to handle cases like backspace, autofill or repeated searches.
// Ensure to clear those internal caches when switching tabs.
this.controller.resetInternalState();
break;
}
]]></body>
</method>
<!--
onBeforeTextValueSet is called by the base-binding's .textValue getter.
It should return the value that the getter should use.
-->
<method name="onBeforeTextValueGet">
<body><![CDATA[
return { value: this.inputField.value };
]]></body>
</method>
<!--
onBeforeTextValueSet is called by the base-binding's .textValue setter.
It should return the value that the setter should use.
-->
<method name="onBeforeTextValueSet">
<parameter name="aValue"/>
<body><![CDATA[
let val = aValue;
let uri;
try {
uri = makeURI(val);
} catch (ex) {}
if (uri) {
// Do not touch moz-action URIs at all. They depend on being
// properly encoded and decoded and will break if decoded
// unexpectedly.
if (!this._parseActionUrl(val)) {
val = losslessDecodeURI(uri);
}
}
return val;
]]></body>
</method>
<method name="_parseActionUrl">
<parameter name="aUrl"/>
<body><![CDATA[
const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
if (!MOZ_ACTION_REGEX.test(aUrl))
return null;
// URL is in the format moz-action:ACTION,PARAMS
// Where PARAMS is a JSON encoded object.
let [, type, params] = aUrl.match(MOZ_ACTION_REGEX);
let action = {
type,
};
action.params = JSON.parse(params);
for (let key in action.params) {
action.params[key] = decodeURIComponent(action.params[key]);
}
if ("url" in action.params) {
let uri;
try {
uri = makeURI(action.params.url);
action.params.displayUrl = losslessDecodeURI(uri);
} catch (e) {
action.params.displayUrl = action.params.url;
}
}
return action;
]]></body>
</method>
<property name="_noActionKeys" readonly="true">
<getter><![CDATA[
if (!this.__noActionKeys) {
this.__noActionKeys = new Set([
KeyEvent.DOM_VK_ALT,
KeyEvent.DOM_VK_SHIFT,
]);
let modifier = AppConstants.platform == "macosx" ?
KeyEvent.DOM_VK_META :
KeyEvent.DOM_VK_CONTROL;
this.__noActionKeys.add(modifier);
}
return this.__noActionKeys;
]]></getter>
</property>
<field name="_pressedNoActionKeys"><![CDATA[
new Set()
]]></field>
<method name="_clearNoActions">
<parameter name="aURL"/>
<body><![CDATA[
this._pressedNoActionKeys.clear();
this.popup.removeAttribute("noactions");
let action = this._parseActionUrl(this._value);
if (action)
this.setAttribute("actiontype", action.type);
]]></body>
</method>
<method name="onInput">
<parameter name="aEvent"/>
<body><![CDATA[
if (!this.mIgnoreInput && this.mController.input == this) {
this._value = this.inputField.value;
gBrowser.userTypedValue = this.value;
this.valueIsTyped = true;
if (this.inputField.value) {
this.setAttribute("usertyping", "true");
} else {
this.removeAttribute("usertyping");
}
// Only wait for a result when we are sure to get one. In some
// cases, like when pasting the same exact text, we may not fire
// a new search and we won't get a result.
if (this.mController.handleText()) {
this.gotResultForCurrentQuery = false;
this._searchStartDate = Cu.now();
this._deferredKeyEventQueue = [];
if (this._deferredKeyEventTimeout) {
clearTimeout(this._deferredKeyEventTimeout);
this._deferredKeyEventTimeout = null;
}
}
}
this.resetActionType();
]]></body>
</method>
<method name="handleEnter">
<parameter name="event"/>
<parameter name="noDefer"/>
<body><![CDATA[
// We need to ensure we're using a selected autocomplete result.
// A result should automatically be selected by default,
// however autocomplete is async and therefore we may not
// have a result set relating to the current input yet. If that
// happens, we need to mark that when the first result does get added,
// it needs to be handled as if enter was pressed with that first
// result selected.
// If anything other than the default (first) result is selected, then
// it must have been manually selected by the human. We let this
// explicit choice be used, even if it may be related to a previous
// input.
// However, if the default result is automatically selected, we
// ensure that it corresponds to the current input.
// Store the current search string so it can be used in handleCommand,
// which will be called as a result of mController.handleEnter().
this.handleEnterSearchString = this.mController.searchString;
if (!noDefer && this._shouldDeferKeyEvent(event)) {
// Defer the event until the first non-heuristic result comes in.
this._deferKeyEvent(event, "handleEnter");
return false;
}
let canonizeValue = this.value;
if (event.shiftKey || (AppConstants.platform === "macosx" ?
event.metaKey :
event.ctrlKey)) {
let action = this._parseActionUrl(canonizeValue);
if (action && "searchSuggestion" in action.params) {
canonizeValue = action.params.searchSuggestion;
} else if (this.popup.selectedIndex === 0 &&
this.mController.getStyleAt(0).includes("autofill")) {
canonizeValue = this.handleEnterSearchString;
}
}
this.maybeCanonizeURL(event, canonizeValue);
let handled = this.mController.handleEnter(false, event);
this.handleEnterSearchString = null;
this.popup.overrideValue = null;
return handled;
]]></body>
</method>
<method name="handleDelete">
<body><![CDATA[
// If the heuristic result is selected, then the autocomplete
// controller's handleDelete implementation will remove it, which is
// not what we want. So in that case, call handleText so it acts as
// a backspace on the text value instead of removing the result.
if (this.popup.selectedIndex == 0 &&
this.popup._isFirstResultHeuristic) {
this.mController.handleText();
return false;
}
return this.mController.handleDelete();
]]></body>
</method>
<property name="_userMadeSearchSuggestionsChoice" readonly="true">
<getter><![CDATA[
return this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
this._defaultPrefs.getBoolPref("suggest.searches") != this._prefs.getBoolPref("suggest.searches");
]]></getter>
</property>
<property name="whichSearchSuggestionsNotification" readonly="true">
<getter><![CDATA[
// Once we return "none" once, we'll always return "none".
// If available, use the cached value, rather than running all of the
// checks again at every locationbar focus.
if (this._whichSearchSuggestionsNotification) {
return this._whichSearchSuggestionsNotification;
}
if (this.browserSearchSuggestEnabled && !this.inPrivateContext &&
// In any case, if the user made a choice we should not nag him.
!this._userMadeSearchSuggestionsChoice) {
if (this._defaultPrefs.getBoolPref("suggest.searches") &&
this.urlbarSearchSuggestEnabled && // Has not been switched off.
this._prefs.getIntPref("timesBeforeHidingSuggestionsHint")) {
return "opt-out";
}
}
return this._whichSearchSuggestionsNotification = "none";
]]></getter>
</property>
<method name="updateSearchSuggestionsNotificationImpressions">
<parameter name="whichNotification"/>
<body><![CDATA[
if (whichNotification == "none") {
throw new Error("Unexpected notification type");
}
let remaining = this._prefs.getIntPref("timesBeforeHidingSuggestionsHint");
if (remaining > 0) {
this._prefs.setIntPref("timesBeforeHidingSuggestionsHint", remaining - 1);
}
]]></body>
</method>
<method name="maybeShowSearchSuggestionsNotificationOnFocus">
<parameter name="mouseFocused"/>
<body><![CDATA[
let whichNotification = this.whichSearchSuggestionsNotification;
if (this._showSearchSuggestionNotificationOnMouseFocus &&
mouseFocused) {
// Force showing the opt-out notification.
this._whichSearchSuggestionsNotification = whichNotification = "opt-out";
}
if (whichNotification == "opt-out") {
try {
this.popup.openAutocompletePopup(this, this);
} finally {
if (mouseFocused) {
delete this._whichSearchSuggestionsNotification;
this._showSearchSuggestionNotificationOnMouseFocus = false;
}
}
}
]]></body>
</method>
</implementation>
<handlers>
<handler event="keydown"><![CDATA[
if (this._noActionKeys.has(event.keyCode) &&
this.popup.selectedIndex >= 0 &&
!this._pressedNoActionKeys.has(event.keyCode)) {
if (this._pressedNoActionKeys.size == 0) {
this.popup.setAttribute("noactions", "true");
this.removeAttribute("actiontype");
}
this._pressedNoActionKeys.add(event.keyCode);
}
]]></handler>
<handler event="keyup"><![CDATA[
if (this._noActionKeys.has(event.keyCode) &&
this._pressedNoActionKeys.has(event.keyCode)) {
this._pressedNoActionKeys.delete(event.keyCode);
if (this._pressedNoActionKeys.size == 0)
this._clearNoActions();
}
]]></handler>
<handler event="mousedown"><![CDATA[
if (event.button == 0) {
if (event.originalTarget.getAttribute("anonid") == "historydropmarker") {
this.toggleHistoryPopup();
}
// Eventually show the opt-out notification even if the location bar is
// empty, focused, and the user clicks on it.
if (this.focused && this.textValue == "") {
this.maybeShowSearchSuggestionsNotificationOnFocus(true);
}
}
]]></handler>
<handler event="focus"><![CDATA[
if (event.originalTarget == this.inputField) {
this._hideURLTooltip();
this.formatValue();
if (this.getAttribute("pageproxystate") != "valid") {
UpdatePopupNotificationsVisibility();
}
// We show the opt-out notification when the mouse/keyboard focus the
// urlbar, but in any case we want to enforce at least one
// notification when the user focuses it with the mouse.
let whichNotification = this.whichSearchSuggestionsNotification;
if (whichNotification == "opt-out" &&
this._showSearchSuggestionNotificationOnMouseFocus === undefined) {
this._showSearchSuggestionNotificationOnMouseFocus = true;
}
// Check whether the focus change came from a keyboard/mouse action.
let focusMethod = Services.focus.getLastFocusMethod(window);
// If it's a focus started by code and the primary user intention was
// not to go to the location bar, don't show a notification.
if (!focusMethod && !this.userInitiatedFocus) {
return;
}
let mouseFocused = !!(focusMethod & Services.focus.FLAG_BYMOUSE);
this.maybeShowSearchSuggestionsNotificationOnFocus(mouseFocused);
}
]]></handler>
<handler event="blur"><![CDATA[
if (event.originalTarget == this.inputField) {
this._clearNoActions();
this.formatValue();
if (this.getAttribute("pageproxystate") != "valid") {
UpdatePopupNotificationsVisibility();
}
}
if (this.ExtensionSearchHandler.hasActiveInputSession()) {
this.ExtensionSearchHandler.handleInputCancelled();
}
if (this._deferredKeyEventTimeout) {
clearTimeout(this._deferredKeyEventTimeout);
this._deferredKeyEventTimeout = null;
}
this._deferredKeyEventQueue = [];
]]></handler>
<handler event="dragstart" phase="capturing"><![CDATA[
// Drag only if the gesture starts from the input field.
if (this.inputField != event.originalTarget &&
!(this.inputField.compareDocumentPosition(event.originalTarget) &
Node.DOCUMENT_POSITION_CONTAINED_BY))
return;
// Drag only if the entire value is selected and it's a valid URI.
var isFullSelection = this.selectionStart == 0 &&
this.selectionEnd == this.textLength;
if (!isFullSelection ||
this.getAttribute("pageproxystate") != "valid")
return;
var urlString = gBrowser.selectedBrowser.currentURI.displaySpec;
var title = gBrowser.selectedBrowser.contentTitle || urlString;
var htmlString = "<a href=\"" + urlString + "\">" + urlString + "</a>";
var dt = event.dataTransfer;
dt.setData("text/x-moz-url", urlString + "\n" + title);
dt.setData("text/unicode", urlString);
dt.setData("text/html", htmlString);
dt.effectAllowed = "copyLink";
event.stopPropagation();
]]></handler>
<handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/>
<handler event="drop" phase="capturing" action="this.onDrop(event, this);"/>
<handler event="select"><![CDATA[
if (!Cc["@mozilla.org/widget/clipboard;1"]
.getService(Ci.nsIClipboard)
.supportsSelectionClipboard())
return;
if (!window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.isHandlingUserInput)
return;
var val = this._getSelectedValueForClipboard();
if (!val)
return;
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard);
]]></handler>
</handlers>
</binding>
<binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
<content ignorekeys="true" level="top" consumeoutsideclicks="never"
aria-owns="richlistbox">
<xul:deck anonid="search-suggestions-notification"
align="center"
role="alert"
selectedIndex="0">
<!-- OPT-OUT -->
<xul:hbox flex="1" align="center" anonid="search-suggestions-opt-out">
<xul:image class="ac-site-icon" type="searchengine"/>
<xul:hbox anonid="search-suggestions-hint-typing">
<xul:description class="ac-title-text">&brandShortName;</xul:description>
</xul:hbox>
<xul:hbox anonid="search-suggestions-hint-box" flex="1">
<xul:description id="search-suggestions-hint">
<html:span class="prefix">&#x1f4a1; &urlbar.searchSuggestionsNotification.hintPrefix;</html:span>
<html:span>&urlbar.searchSuggestionsNotification.hint;</html:span>
</xul:description>
</xul:hbox>
<xul:label id="search-suggestions-change-settings"
class="text-link"
role="link"
#ifdef XP_WIN
value="&urlbar.searchSuggestionsNotification.changeSettingsWin;"
accesskey="&urlbar.searchSuggestionsNotification.changeSettingsWin.accesskey;"
#else
value="&urlbar.searchSuggestionsNotification.changeSettingsUnix;"
accesskey="&urlbar.searchSuggestionsNotification.changeSettingsUnix.accesskey;"
#endif
onclick="openPreferences('paneSearch', {origin: 'searchChangeSettings'});"
control="search-suggestions-change-settings"/>
</xul:hbox>
</xul:deck>
<xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
flex="1"/>
<xul:hbox anonid="footer">
<children/>
<xul:vbox anonid="one-off-search-buttons"
class="search-one-offs"
compact="true"
includecurrentengine="true"
disabletab="true"
flex="1"/>
</xul:hbox>
</content>
<implementation>
<!--
For performance reasons we want to limit the size of the text runs we
build and show to the user.
-->
<field name="textRunsMaxLen">255</field>
<field name="DOMWindowUtils">
window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
</field>
<field name="_maxResults">0</field>
<field name="_bundle" readonly="true">
Cc["@mozilla.org/intl/stringbundle;1"].
getService(Ci.nsIStringBundleService).
createBundle("chrome://browser/locale/places/places.properties");
</field>
<field name="searchSuggestionsNotification" readonly="true">
document.getAnonymousElementByAttribute(
this, "anonid", "search-suggestions-notification"
);
</field>
<field name="footer" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "footer");
</field>
<field name="oneOffSearchButtons" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid",
"one-off-search-buttons");
</field>
<field name="_oneOffSearchesEnabled">false</field>
<field name="_overrideValue">null</field>
<property name="overrideValue"
onget="return this._overrideValue;"
onset="this._overrideValue = val; return val;"/>
<method name="onPopupClick">
<parameter name="aEvent"/>
<body><![CDATA[
if (aEvent.button == 2) {
// Ignore right-clicks.
return;
}
// Otherwise "call super" -- do what autocomplete-base-popup does.
this.input.controller.handleEnter(true, aEvent);
]]></body>
</method>
<method name="enableOneOffSearches">
<parameter name="enable"/>
<body><![CDATA[
this._oneOffSearchesEnabled = enable;
if (enable) {
this.oneOffSearchButtons.telemetryOrigin = "urlbar";
this.oneOffSearchButtons.style.display = "-moz-box";
// Set .textbox first, since the popup setter will cause
// a _rebuild call that uses it.
this.oneOffSearchButtons.textbox = this.input;
this.oneOffSearchButtons.popup = this;
} else {
this.oneOffSearchButtons.telemetryOrigin = null;
this.oneOffSearchButtons.style.display = "none";
this.oneOffSearchButtons.textbox = null;
this.oneOffSearchButtons.popup = null;
}
]]></body>
</method>
<!-- Override this so that navigating between items results in an item
always being selected. -->
<method name="getNextIndex">
<parameter name="reverse"/>
<parameter name="amount"/>
<parameter name="index"/>
<parameter name="maxRow"/>
<body><![CDATA[
if (maxRow < 0)
return -1;
let newIndex = index + (reverse ? -1 : 1) * amount;
// We only want to wrap if navigation is in any direction by one item,
// otherwise we clamp to one end of the list.
// ie, hitting page-down will only cause is to wrap if we're already
// at one end of the list.
// Allow the selection to be removed if the first result is not a
// heuristic result.
if (!this._isFirstResultHeuristic) {
if (reverse && index == -1 || newIndex > maxRow && index != maxRow)
newIndex = maxRow;
else if (!reverse && index == -1 || newIndex < 0 && index != 0)
newIndex = 0;
if (newIndex < 0 && index == 0 || newIndex > maxRow && index == maxRow)
newIndex = -1;
return newIndex;
}
// Otherwise do not allow the selection to be removed.
if (newIndex < 0) {
newIndex = index > 0 ? 0 : maxRow;
} else if (newIndex > maxRow) {
newIndex = index < maxRow ? maxRow : 0;
}
return newIndex;
]]></body>
</method>
<property name="_isFirstResultHeuristic" readonly="true">
<getter>
<![CDATA[
// The popup usually has a special "heuristic" first result (added
// by UnifiedComplete.js) that is automatically selected when the
// popup opens.
return this.input.mController.matchCount > 0 &&
this.input.mController
.getStyleAt(0)
.split(/\s+/).indexOf("heuristic") > 0;
]]>
</getter>
</property>
<property name="maxResults">
<getter>
<![CDATA[
if (!this._maxResults) {
this._maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
}
return this._maxResults;
]]>
</getter>
<setter>
<![CDATA[
return this._maxResults = parseInt(val);
]]>
</setter>
</property>
<!-- This is set either to undefined or to a new object containing
{ start, end } margin values in pixels. These are used to align the
results to the input field. -->
<property name="margins"
onget="return this._margins;">
<setter>
<![CDATA[
this._margins = val;
if (val) {
/* eslint-disable no-multi-spaces */
let paddingInCSS =
3 // .autocomplete-richlistbox padding-left/right
+ 6 // .ac-site-icon margin-inline-start
+ 16 // .ac-site-icon width
+ 6; // .ac-site-icon margin-inline-end
/* eslint-enable no-multi-spaces */
let actualVal = Math.round(val.start) - paddingInCSS;
let actualValEnd = Math.round(val.end);
this.style.setProperty("--item-padding-start", actualVal + "px");
this.style.setProperty("--item-padding-end", actualValEnd + "px");
} else {
this.style.removeProperty("--item-padding-start");
this.style.removeProperty("--item-padding-end");
}
return val;
]]>
</setter>
</property>
<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._openAutocompletePopup(aInput, aElement);
]]>
</body>
</method>
<method name="_openAutocompletePopup">
<parameter name="aInput"/>
<parameter name="aElement"/>
<body><![CDATA[
if (this.mPopupOpen) {
return;
}
// Set the direction of the popup based on the textbox (bug 649840).
// getComputedStyle causes a layout flush, so avoid calling it if a
// direction has already been set.
if (!this.style.direction) {
this.style.direction =
aElement.ownerGlobal.getComputedStyle(aElement).direction;
}
let popupDirection = this.style.direction;
// Make the popup span the width of the window. First, set its width.
let documentRect =
this.DOMWindowUtils
.getBoundsWithoutFlushing(window.document.documentElement);
let width = documentRect.right - documentRect.left;
this.setAttribute("width", width);
// Now make its starting margin negative so that its leading edge
// aligns with the window border.
let elementRect =
this.DOMWindowUtils.getBoundsWithoutFlushing(aElement);
if (popupDirection == "rtl") {
let offset = elementRect.right - documentRect.right;
this.style.marginRight = offset + "px";
} else {
let offset = documentRect.left - elementRect.left;
this.style.marginLeft = offset + "px";
}
// Keep the popup items' site icons aligned with the urlbar's identity
// icon if it's not too far from the edge of the window. We define
// "too far" as "more than 30% of the window's width AND more than
// 250px". Do this *before* adding any items because when the new
// value of the margins are different from the previous value, over-
// and underflow must be handled for each item already in the popup.
let needsHandleOverUnderflow = false;
let boundToCheck = popupDirection == "rtl" ? "right" : "left";
let inputRect = this.DOMWindowUtils.getBoundsWithoutFlushing(aInput);
let startOffset = Math.abs(inputRect[boundToCheck] - documentRect[boundToCheck]);
let alignSiteIcons = startOffset / width <= 0.3 || startOffset <= 250;
if (alignSiteIcons) {
// Calculate the end margin if we have a start margin.
let boundToCheckEnd = popupDirection == "rtl" ? "left" : "right";
let endOffset = Math.abs(inputRect[boundToCheckEnd] -
documentRect[boundToCheckEnd]);
if (endOffset > startOffset * 2) {
// Provide more space when aligning would result in an unbalanced
// margin. This allows the location bar to be moved to the start
// of the navigation toolbar to reclaim space for results.
endOffset = startOffset;
}
let identityIcon = document.getElementById("identity-icon");
let identityRect =
this.DOMWindowUtils.getBoundsWithoutFlushing(identityIcon);
let start = popupDirection == "rtl" ?
documentRect.right - identityRect.right :
identityRect.left;
if (!this.margins || start != this.margins.start ||
endOffset != this.margins.end ||
width != this.margins.width) {
this.margins = { start, end: endOffset, width };
needsHandleOverUnderflow = true;
}
} else if (this.margins) {
// Reset the alignment so that the site icons are positioned
// according to whatever's in the CSS.
this.margins = undefined;
needsHandleOverUnderflow = true;
}
// Now that the margins have been set, start adding items (via
// _invalidate).
this.mInput = aInput;
this.input.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1);
this.input.userSelectionBehavior = "none";
this._invalidate();
try {
let whichNotification = aInput.whichSearchSuggestionsNotification;
if (whichNotification != "none") {
// Update the impressions count on real popupshown, since there's
// no guarantee openPopup will be respected by the platform.
// Though, we must ensure the handled event is the expected one.
let impressionId = this._searchSuggestionsImpressionId = {};
this.addEventListener("popupshown", () => {
if (this._searchSuggestionsImpressionId == impressionId)
aInput.updateSearchSuggestionsNotificationImpressions(whichNotification);
}, {once: true});
this._showSearchSuggestionsNotification(whichNotification, popupDirection);
} else if (this.classList.contains("showSearchSuggestionsNotification")) {
this._hideSearchSuggestionsNotification();
}
} catch (ex) {
// Not critical for the urlbar functionality, just report the error.
Cu.reportError(ex);
}
// Position the popup below the navbar. To get the y-coordinate,
// which is an offset from the bottom of the input, subtract the
// bottom of the navbar from the buttom of the input.
let yOffset = Math.round(
this.DOMWindowUtils.getBoundsWithoutFlushing(document.getElementById("nav-bar")).bottom -
this.DOMWindowUtils.getBoundsWithoutFlushing(aInput).bottom);
this.openPopup(aElement, "after_start", 0, yOffset, false, false);
// Do this immediately after we've requested the popup to open. This
// will cause sync reflows but prevents flickering.
if (needsHandleOverUnderflow) {
for (let item of this.richlistbox.childNodes) {
item.handleOverUnderflow();
}
}
]]></body>
</method>
<method name="_showSearchSuggestionsNotification">
<parameter name="whichNotification"/>
<parameter name="popupDirection"/>
<body>
<![CDATA[
if (whichNotification == "opt-out") {
if (this.margins) {
this.searchSuggestionsNotification.style.paddingInlineStart =
this.margins.start + "px";
} else {
this.searchSuggestionsNotification.style.removeProperty("padding-inline-start");
}
// We want to animate the opt-out hint only once.
if (!this._firstSearchSuggestionsNotification) {
this._firstSearchSuggestionsNotification = true;
this.searchSuggestionsNotification.setAttribute("animate", "true");
}
}
this.searchSuggestionsNotification.setAttribute("aria-describedby",
"search-suggestions-hint");
// With the notification shown, the listbox's height can sometimes be
// too small when it's flexed, as it normally is. Also, it can start
// out slightly scrolled down. Both problems appear together, most
// often when the popup is very narrow and the notification's text
// must wrap. Work around them by removing the flex.
//
// But without flexing the listbox, the listbox's height animation
// sometimes fails to complete, leaving the popup too tall. Work
// around that problem by disabling the listbox animation.
this.richlistbox.flex = 0;
this.setAttribute("dontanimate", "true");
this.classList.add("showSearchSuggestionsNotification");
// Don't show the one-off buttons if we are showing onboarding and
// there's no result, since it would be ugly and pointless.
this.footer.collapsed = this.matchCount == 0;
this.input.tabScrolling = this.matchCount != 0;
// This event allows accessibility APIs to see the notification.
if (!this.popupOpen) {
let event = document.createEvent("Events");
event.initEvent("AlertActive", true, true);
this.searchSuggestionsNotification.dispatchEvent(event);
}
]]>
</body>
</method>
<method name="_hideSearchSuggestionsNotification">
<body>
<![CDATA[
this.classList.remove("showSearchSuggestionsNotification");
this.richlistbox.flex = 1;
this.removeAttribute("dontanimate");
this.searchSuggestionsNotification.removeAttribute("animate");
if (this.matchCount) {
// Update popup height.
this._invalidate();
} else {
this.closePopup();
}
]]>
</body>
</method>
<method name="_selectedOneOffChanged">
<body><![CDATA[
// Update all searchengine result items to use the newly selected
// engine.
for (let item of this.richlistbox.childNodes) {
if (item.collapsed) {
break;
}
let url = item.getAttribute("url");
if (url) {
let action = item._parseActionUrl(url);
if (action && action.type == "searchengine") {
item._adjustAcItem();
}
}
}
]]></body>
</method>
<!-- This handles keypress changes to the selection among the one-off
search buttons and between the one-offs and the listbox. It returns
true if the keypress was consumed and false if not. -->
<method name="handleKeyPress">
<parameter name="aEvent"/>
<body><![CDATA[
this.oneOffSearchButtons.handleKeyPress(aEvent, this.matchCount,
!this._isFirstResultHeuristic,
gBrowser.userTypedValue);
return aEvent.defaultPrevented && !aEvent.urlbarDeferred;
]]></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[
this.input.handleCommand(event, where, params);
]]></body>
</method>
<!-- Result listitems call this to determine which search engine they
should show in their labels and include in their url attributes. -->
<property name="overrideSearchEngineName" readonly="true">
<getter><![CDATA[
let button = this.oneOffSearchButtons.selectedButton;
return button && button.engine && button.engine.name;
]]></getter>
</property>
<method name="createResultLabel">
<parameter name="item"/>
<parameter name="proposedLabel"/>
<body>
<![CDATA[
let parts = [proposedLabel];
let action = this.input._parseActionUrl(item.getAttribute("url"));
if (action) {
switch (action.type) {
case "searchengine":
parts = [
action.params.searchSuggestion || action.params.searchQuery,
action.params.engineName,
];
break;
case "switchtab":
case "remotetab":
parts = [
item.getAttribute("title"),
item.getAttribute("displayurl"),
];
break;
}
}
let types = item.getAttribute("type").split(/\s+/);
let type = types.find(t => t != "action" && t != "heuristic");
try {
// Some types intentionally do not map to strings, which is not
// an error.
parts.push(this._bundle.GetStringFromName(type + "ResultLabel"));
} catch (e) {}
return parts.filter(str => str).join(" ");
]]>
</body>
</method>
<method name="maybeSetupSpeculativeConnect">
<parameter name="aUriString"/>
<body><![CDATA[
try {
let uri = makeURI(aUriString);
Services.io.speculativeConnect2(uri, gBrowser.contentPrincipal, null);
} catch (ex) {
// Can't setup speculative connection for this uri string for some
// reason, just ignore it.
}
]]></body>
</method>
<method name="onResultsAdded">
<body>
<![CDATA[
// If nothing is selected yet, select the first result if it is a
// pre-selected "heuristic" result. (See UnifiedComplete.js.)
if (this.selectedIndex == -1 && this._isFirstResultHeuristic) {
// Don't fire DOMMenuItemActive so that screen readers still see
// the input as being focused.
this.richlistbox.suppressMenuItemEvent = true;
this.input.controller.setInitiallySelectedIndex(0);
this.richlistbox.suppressMenuItemEvent = false;
}
// If this is the first time we get the result from the current
// search and we are not in the private context, we can speculatively
// connect to the intended site as a performance optimization.
if (!this.input.gotResultForCurrentQuery &&
this.input.speculativeConnectEnabled &&
!this.input.inPrivateContext &&
this.input.mController.matchCount > 0) {
let firstStyle = this.input.mController.getStyleAt(0);
if (firstStyle.includes("autofill")) {
let uri = this.input.mController.getFinalCompleteValueAt(0);
this.maybeSetupSpeculativeConnect(uri);
} else if (firstStyle.includes("searchengine") &&
this.input.browserSearchSuggestEnabled &&
this.input.urlbarSearchSuggestEnabled) {
// Preconnect to the current search engine only if the search
// suggestions are enabled.
let engine = Services.search.currentEngine;
engine.speculativeConnect({window,
originAttributes: gBrowser.contentPrincipal.originAttributes});
}
}
// When a result is present the footer should always be visible.
this.footer.collapsed = false;
this.input.tabScrolling = true;
this.input.gotResultForCurrentQuery = true;
this.input.replaySafeDeferredKeyEvents();
]]>
</body>
</method>
<method name="_onSearchBegin">
<body><![CDATA[
// Set the selected index to 0 (heuristic) until a result comes back
// and we can evaluate it better.
//
// This is required to properly manage delayed handleEnter:
// 1. if a search starts we set selectedIndex to 0 here, and it will
// be updated by onResultsAdded. Since selectedIndex is 0,
// handleEnter will delay the action if a result didn't arrive yet.
// 2. if a search doesn't start (for example if autocomplete is
// disabled), this won't be called, and the selectedIndex will be
// the default -1 value. Then handleEnter will know it should not
// delay the action, cause a result wont't ever arrive.
this.input.controller.setInitiallySelectedIndex(0);
// Since we are starting a new search, reset the currently selected
// one-off button, to cover those cases where the oneoff buttons
// binding won't receive an actual DOM event. For example, a search
// could be started without an actual input event, and the popup may
// not have been closed from the previous search.
this.oneOffSearchButtons.selectedButton = null;
]]></body>
</method>
<field name="_addonIframe">null</field>
<field name="_addonIframeOwner">null</field>
<field name="_addonIframeOverriddenFunctionsByName">{}</field>
<!-- These methods must be overridden and properly handled by the API
runtime so that it doesn't break the popup. If any of these methods
is not overridden, then initAddonIframe should throw. -->
<field name="_addonIframeOverrideFunctionNames">[
"_invalidate",
]</field>
<field name="_addonIframeHiddenAnonids">[
"search-suggestions-notification",
"richlistbox",
"one-off-search-buttons",
]</field>
<field name="_addonIframeHiddenDisplaysByAnonid">{}</field>
<method name="initAddonIframe">
<parameter name="owner"/>
<parameter name="overrides"/>
<body><![CDATA[
if (this._addonIframeOwner) {
// Another add-on has already requested the iframe. Return null to
// signal to the calling add-on that it should not take over the
// popup. First add-on wins for now.
return null;
}
// Make sure all overrides are provided before doing anything.
for (let name of this._addonIframeOverrideFunctionNames) {
if (typeof(overrides[name]) != "function") {
throw new Error(
"Override for method '" + name + "' must be given"
);
}
}
// OK, insert the iframe.
this._addonIframeOwner = owner;
this._addonIframe = this._makeAddonIframe();
this._addonIframeOverriddenFunctionsByName = {};
for (let name of this._addonIframeOverrideFunctionNames) {
this._addonIframeOverriddenFunctionsByName[name] = this[name];
this[name] = overrides[name];
}
return this._addonIframe;
]]></body>
</method>
<method name="destroyAddonIframe">
<parameter name="owner"/>
<body><![CDATA[
if (this._addonIframeOwner != owner) {
throw new Error("You're not the iframe owner");
}
this._addonIframeOwner = null;
this._addonIframe.remove();
this._addonIframe = null;
for (let anonid of this._addonIframeHiddenAnonids) {
let child = document.getAnonymousElementByAttribute(
this, "anonid", anonid
);
child.style.display =
this._addonIframeHiddenDisplaysByAnonid[anonid];
}
for (let name in this._addonIframeOverriddenFunctionsByName) {
this[name] = this._addonIframeOverriddenFunctionsByName[name];
}
this._addonIframeOverriddenFunctionsByName = {};
]]></body>
</method>
<method name="_makeAddonIframe">
<body><![CDATA[
this._addonIframeHiddenDisplaysByAnonid = {};
for (let anonid of this._addonIframeHiddenAnonids) {
let child = document.getAnonymousElementByAttribute(
this, "anonid", anonid
);
this._addonIframeHiddenDisplaysByAnonid[anonid] =
child.style.display;
child.style.display = "none";
}
let XUL_NS =
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
let iframe = document.createElementNS(XUL_NS, "iframe");
iframe.setAttribute("type", "content");
iframe.setAttribute("flex", "1");
iframe.style.transition = "height 100ms";
this.appendChild(iframe);
return iframe;
]]></body>
</method>
</implementation>
<handlers>
<handler event="SelectedOneOffButtonChanged"><![CDATA[
this._selectedOneOffChanged();
]]></handler>
<handler event="mousedown"><![CDATA[
// Required to make the xul:label.text-link elements in the search
// suggestions notification work correctly when clicked on Linux.
// This is copied from the mousedown handler in
// browser-search-autocomplete-result-popup, which apparently had a
// similar problem.
event.preventDefault();
if (event.button == 2) {
// Right mouse button currently allows to select.
this.input.userSelectionBehavior = "rightClick";
// Ignore right-clicks.
return;
}
if (!this.input.speculativeConnectEnabled) {
return;
}
// Ensure the user is clicking on an url instead of other buttons
// on the popup.
let elt = event.originalTarget;
while (elt && elt.localName != "richlistitem" && elt != this) {
elt = elt.parentNode;
}
if (!elt || elt.localName != "richlistitem") {
return;
}
// The user might click on a ghost entry which was removed because of
// the coming new results.
if (this.input.controller.matchCount <= this.selectedIndex) {
return;
}
let url = this.input.controller.getFinalCompleteValueAt(this.selectedIndex);
// Whitelist the cases that we want to speculative connect, and ignore
// other moz-action uris or fancy protocols.
// Note that it's likely we've speculatively connected to the first
// url because it is a heuristic "autofill" result (see bug 1348275).
// "moz-action:searchengine" is also the same case. (see bug 1355443)
// So we won't duplicate the effort here.
if (url.startsWith("http") && this.selectedIndex > 0) {
this.maybeSetupSpeculativeConnect(url);
} else if (url.startsWith("moz-action:remotetab")) {
// URL is in the format moz-action:ACTION,PARAMS
// Where PARAMS is a JSON encoded object.
const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
if (!MOZ_ACTION_REGEX.test(url))
return;
let params = JSON.parse(url.match(MOZ_ACTION_REGEX)[2]);
if (params.url) {
this.maybeSetupSpeculativeConnect(decodeURIComponent(params.url));
}
}
]]></handler>
</handlers>
</binding>
<binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
<implementation>
<constructor><![CDATA[
if (!this.notification)
return;
this.notification.options.installs.forEach(function(aInstall) {
aInstall.addListener(this);
}, this);
// Calling updateProgress can sometimes cause this notification to be
// removed in the middle of refreshing the notification panel which
// makes the panel get refreshed again. Just initialise to the
// undetermined state and then schedule a proper check at the next
// opportunity
this.setProgress(0, -1);
this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0);
]]></constructor>
<destructor><![CDATA[
this.destroy();
]]></destructor>
<field name="progressmeter" readonly="true">
document.getElementById("addon-progress-notification-progressmeter");
</field>
<field name="progresstext" readonly="true">
document.getElementById("addon-progress-notification-progresstext");
</field>
<property name="DownloadUtils" readonly="true">
<getter><![CDATA[
let module = {};
ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", module);
Object.defineProperty(this, "DownloadUtils", {
configurable: true,
enumerable: true,
writable: true,
value: module.DownloadUtils
});
return module.DownloadUtils;
]]></getter>
</property>
<method name="destroy">
<body><![CDATA[
if (!this.notification)
return;
this.notification.options.installs.forEach(function(aInstall) {
aInstall.removeListener(this);
}, this);
clearTimeout(this._updateProgressTimeout);
]]></body>
</method>
<method name="setProgress">
<parameter name="aProgress"/>
<parameter name="aMaxProgress"/>
<body><![CDATA[
if (aMaxProgress == -1) {
this.progressmeter.setAttribute("mode", "undetermined");
} else {
this.progressmeter.setAttribute("mode", "determined");
this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress);
}
let now = Date.now();
if (!this.notification.lastUpdate) {
this.notification.lastUpdate = now;
this.notification.lastProgress = aProgress;
return;
}
let delta = now - this.notification.lastUpdate;
if ((delta < 400) && (aProgress < aMaxProgress))
return;
delta /= 1000;
// This algorithm is the same used by the downloads code.
let speed = (aProgress - this.notification.lastProgress) / delta;
if (this.notification.speed)
speed = speed * 0.9 + this.notification.speed * 0.1;
this.notification.lastUpdate = now;
this.notification.lastProgress = aProgress;
this.notification.speed = speed;
let status = null;
[status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last);
this.progresstext.setAttribute("value", status);
this.progresstext.setAttribute("tooltiptext", status);
]]></body>
</method>
<method name="cancel">
<body><![CDATA[
let installs = this.notification.options.installs;
installs.forEach(function(aInstall) {
try {
aInstall.cancel();
} catch (e) {
// Cancel will throw if the download has already failed
}
}, this);
PopupNotifications.remove(this.notification);
]]></body>
</method>
<method name="updateProgress">
<body><![CDATA[
if (!this.notification)
return;
let downloadingCount = 0;
let progress = 0;
let maxProgress = 0;
this.notification.options.installs.forEach(function(aInstall) {
if (aInstall.maxProgress == -1)
maxProgress = -1;
progress += aInstall.progress;
if (maxProgress >= 0)
maxProgress += aInstall.maxProgress;
if (aInstall.state < AddonManager.STATE_DOWNLOADED)
downloadingCount++;
});
if (downloadingCount == 0) {
this.destroy();
if (Services.prefs.getBoolPref("xpinstall.customConfirmationUI", false)) {
this.progressmeter.setAttribute("mode", "undetermined");
let status = gNavigatorBundle.getString("addonDownloadVerifying");
this.progresstext.setAttribute("value", status);
this.progresstext.setAttribute("tooltiptext", status);
} else {
PopupNotifications.remove(this.notification);
}
} else {
this.setProgress(progress, maxProgress);
}
]]></body>
</method>
<method name="onDownloadProgress">
<body><![CDATA[
this.updateProgress();
]]></body>
</method>
<method name="onDownloadFailed">
<body><![CDATA[
this.updateProgress();
]]></body>
</method>
<method name="onDownloadCancelled">
<body><![CDATA[
this.updateProgress();
]]></body>
</method>
<method name="onDownloadEnded">
<body><![CDATA[
this.updateProgress();
]]></body>
</method>
</implementation>
</binding>
</bindings>