зеркало из https://github.com/mozilla/gecko-dev.git
2855 строки
112 KiB
XML
2855 строки
112 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="scheme"
|
|
class="urlbar-scheme textbox-input"
|
|
required="required"
|
|
xbl:inherits="textoverflow,focused"/>
|
|
<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);
|
|
this.inputField.addEventListener("scrollend", 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();
|
|
}, {capture: 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);
|
|
this.inputField.removeEventListener("scrollend", 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="DOMWindowUtils">
|
|
window.windowUtils;
|
|
</field>
|
|
|
|
<field name="scheme" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "scheme");
|
|
</field>
|
|
|
|
<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[
|
|
// Used to avoid re-entrance in async callbacks.
|
|
let instance = this._formattingInstance = {};
|
|
|
|
if (!this.editor)
|
|
return;
|
|
|
|
// Cleanup previously set styles.
|
|
this.scheme.value = "";
|
|
let controller, strikeOut, selection;
|
|
if (this._formattingEnabled) {
|
|
controller = this.editor.selectionController;
|
|
strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
|
|
strikeOut.removeAllRanges();
|
|
selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
|
|
selection.removeAllRanges();
|
|
this.formatScheme(controller.SELECTION_URLSTRIKEOUT, true);
|
|
this.formatScheme(controller.SELECTION_URLSECONDARY, true);
|
|
this.inputField.style.setProperty("--urlbar-scheme-size", "0px");
|
|
}
|
|
|
|
let textNode = this.editor.rootElement.firstChild;
|
|
let value = textNode.textContent;
|
|
if (!value)
|
|
return;
|
|
|
|
if (this.focused)
|
|
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;
|
|
|
|
let [, preDomain, schemeWSlashes, domain] = matchedURL;
|
|
// We strip http, so we should not show the scheme box for it.
|
|
if (!this._mayTrimURLs || schemeWSlashes != "http://") {
|
|
this.scheme.value = schemeWSlashes;
|
|
this.inputField.style.setProperty("--urlbar-scheme-size",
|
|
schemeWSlashes.length + "ch");
|
|
}
|
|
|
|
// Make sure the host is always visible. Since it is aligned on
|
|
// the first strong directional character, we set the overflow
|
|
// appropriately.
|
|
this.selectionStart = this.selectionEnd = 0;
|
|
window.requestAnimationFrame(() => {
|
|
// Check for re-entrance. On focus change this formatting code is
|
|
// invoked regardless, thus this should be enough.
|
|
if (this._formattingInstance != instance)
|
|
return;
|
|
let isDomainRTL = this.DOMWindowUtils.getDirectionFromText(domain);
|
|
// In the future, for example in bug 525831, we may add a forceRTL
|
|
// char just after the domain, and in such a case we should not
|
|
// scroll to the left.
|
|
if (isDomainRTL && value[preDomain.length + domain.length] != "\u200E") {
|
|
this.inputField.scrollLeft = this.inputField.scrollLeftMax;
|
|
}
|
|
});
|
|
|
|
if (!this._formattingEnabled)
|
|
return;
|
|
|
|
this.formatScheme(controller.SELECTION_URLSECONDARY);
|
|
|
|
// 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);
|
|
this.formatScheme(controller.SELECTION_URLSTRIKEOUT);
|
|
}
|
|
|
|
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="formatScheme">
|
|
<parameter name="selectionType"/>
|
|
<parameter name="clear"/>
|
|
<body><![CDATA[
|
|
let editor = this.scheme.editor;
|
|
let controller = editor.selectionController;
|
|
let textNode = editor.rootElement.firstChild;
|
|
let selection = controller.getSelection(selectionType);
|
|
if (clear) {
|
|
selection.removeAllRanges();
|
|
} else {
|
|
let r = document.createRange();
|
|
r.setStart(textNode, 0);
|
|
r.setEnd(textNode, textNode.textContent.length);
|
|
selection.addRange(r);
|
|
}
|
|
]]></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();
|
|
}
|
|
}
|
|
]]></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._inOverflow = true;
|
|
this.updateTextOverflow();
|
|
break;
|
|
case "underflow":
|
|
this._inOverflow = false;
|
|
this.updateTextOverflow();
|
|
this._hideURLTooltip();
|
|
break;
|
|
case "scrollend":
|
|
this.updateTextOverflow();
|
|
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>
|
|
|
|
<method name="updateTextOverflow">
|
|
<body><![CDATA[
|
|
if (this._inOverflow) {
|
|
window.promiseDocumentFlushed(() => {
|
|
// Check overflow again to ensure it didn't change in the meanwhile.
|
|
let input = this.inputField;
|
|
if (input && this._inOverflow) {
|
|
let side = input.scrollLeft &&
|
|
input.scrollLeft == input.scrollLeftMax ? "start" : "end";
|
|
this.setAttribute("textoverflow", side);
|
|
}
|
|
});
|
|
} else {
|
|
this.removeAttribute("textoverflow");
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<!--
|
|
onBeforeTextValueGet 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.windowUtils.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">💡 &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.windowUtils;
|
|
</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="shrinkDelay" readonly="true">
|
|
250
|
|
</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="adjustHeight">
|
|
<body>
|
|
<![CDATA[
|
|
// If we were going to shrink later, cancel that for now:
|
|
if (this._shrinkTimeout) {
|
|
clearTimeout(this._shrinkTimeout);
|
|
this._shrinkTimeout = null;
|
|
}
|
|
let lastRowCount = this._lastRowCount;
|
|
// Figure out how many rows to show
|
|
let rows = this.richlistbox.childNodes;
|
|
this._lastRowCount = rows.length;
|
|
let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
|
|
|
|
// If we're going from 0 to non-0 rows, we might need to remove
|
|
// the height attribute to allow the popup to size. The attribute
|
|
// is set from XUL popup management code.
|
|
if (!lastRowCount && rows.length) {
|
|
this.removeAttribute("height");
|
|
}
|
|
|
|
// Default the height to 0 if we have no rows to show
|
|
let height = 0;
|
|
if (numRows) {
|
|
if (!this._rowHeight) {
|
|
window.promiseDocumentFlushed(() => {
|
|
if (window.closed) {
|
|
return;
|
|
}
|
|
this._rowHeight = rows[0].getBoundingClientRect().height;
|
|
let style = window.getComputedStyle(this.richlistbox);
|
|
|
|
let paddingTop = parseInt(style.paddingTop) || 0;
|
|
let paddingBottom = parseInt(style.paddingBottom) || 0;
|
|
this._rlbPadding = paddingTop + paddingBottom;
|
|
// Then re-run - but don't dirty layout from inside this callback.
|
|
window.requestAnimationFrame(() => this.adjustHeight());
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Calculate the height to have the first row to last row shown
|
|
height = (this._rowHeight * numRows) + this._rlbPadding;
|
|
}
|
|
|
|
let animate = this.getAttribute("dontanimate") != "true";
|
|
let currentHeight =
|
|
parseFloat(this.richlistbox.getAttribute("height"), 10) ||
|
|
parseFloat(this.richlistbox.style.height, 10) ||
|
|
0; // It's possible we get here when we haven't set height on the richlistbox
|
|
// yet, which means parseFloat will return NaN. It should return 0 instead.
|
|
if (height > currentHeight) {
|
|
// Grow immediately.
|
|
if (animate) {
|
|
this.richlistbox.removeAttribute("height");
|
|
this.richlistbox.style.height = height + "px";
|
|
} else {
|
|
this.richlistbox.style.removeProperty("height");
|
|
this.richlistbox.height = height;
|
|
}
|
|
} else if (height < currentHeight) { // Don't shrink if height matches exactly
|
|
// Delay shrinking to avoid flicker.
|
|
this._shrinkTimeout = setTimeout(() => {
|
|
this._collapseUnusedItems();
|
|
if (animate) {
|
|
this.richlistbox.removeAttribute("height");
|
|
this.richlistbox.style.height = height + "px";
|
|
} else {
|
|
this.richlistbox.style.removeProperty("height");
|
|
this.richlistbox.height = height;
|
|
}
|
|
}, this.shrinkDelay);
|
|
}
|
|
]]>
|
|
</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>
|