forked from mirrors/gecko-dev
This implements the second "thanks for your feedback" UI for weather, Pocket, addon, MDN, etc. suggestions. The first UI is in D175729, which this revision builds on. This UI is shown when a result is dismissed. It's essentially a tip with a smaller icon and padding. It has the "Got it" button that dismisses the tip itself when clicked. I noticed tip top and bottom borders use `--panel-separator-color`, which doesn't seem right. They're supposed to be the same color as the borders between the input and view and between the results and one-off buttons, which is `--autocomplete-popup-separator-color`. The spec for this feature uses the same colors too. So I changed that too. (The use of `--panel-separator-color` goes back to [the initial tip implementation](https://hg.mozilla.org/mozilla-central/rev/78886081d45b09987c1825cc5a160fd6bec61cb8) in 70, but search tips and interventions weren't added until 74, in bug 1606909 and bug 1606917. I checked, and in 74 all the borders are the same color.) References: * [Figma](https://www.figma.com/file/Hdi0oHB7trRcncyVAKZypO/accuweather-explorations?node-id=2421%3A62540&t=svOk7TxQv4V7Y9L4-1) (see "A11y review - user feedback" in sidebar) * [Clickable prototype](https://www.figma.com/proto/Hdi0oHB7trRcncyVAKZypO/accuweather-explorations?page-id=2192%3A42825&node-id=2394-52468&viewport=246%2C526%2C0.12&scaling=min-zoom&starting-point-node-id=2394%3A52468&show-proto-sidebar=1) (see "Revised 4/3" in sidebar) * [Content design](https://docs.google.com/document/d/1Mgt_oAIEDz_sF-YBqqUGtfKHQCAtRzEQohpfqk-2X8U/edit?usp=sharing) Depends on D175729 Differential Revision: https://phabricator.services.mozilla.com/D176468
4132 lines
138 KiB
JavaScript
4132 lines
138 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
|
|
CONTEXTUAL_SERVICES_PING_TYPES:
|
|
"resource:///modules/PartnerLinkAttribution.sys.mjs",
|
|
ExtensionSearchHandler:
|
|
"resource://gre/modules/ExtensionSearchHandler.sys.mjs",
|
|
PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
|
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
|
|
SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
|
|
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
|
|
UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
|
|
UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.sys.mjs",
|
|
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
|
UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs",
|
|
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
|
|
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
|
|
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
|
|
UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.sys.mjs",
|
|
UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(lazy, {
|
|
BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm",
|
|
ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"ClipboardHelper",
|
|
"@mozilla.org/widget/clipboardhelper;1",
|
|
"nsIClipboardHelper"
|
|
);
|
|
|
|
const DEFAULT_FORM_HISTORY_NAME = "searchbar-history";
|
|
const SEARCH_BUTTON_ID = "urlbar-search-button";
|
|
|
|
// The scalar category of TopSites click for Contextual Services
|
|
const SCALAR_CATEGORY_TOPSITES = "contextual.services.topsites.click";
|
|
|
|
let getBoundsWithoutFlushing = element =>
|
|
element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
|
|
let px = number => number.toFixed(2) + "px";
|
|
|
|
/**
|
|
* Implements the text input part of the address bar UI.
|
|
*/
|
|
export class UrlbarInput {
|
|
/**
|
|
* @param {object} options
|
|
* The initial options for UrlbarInput.
|
|
* @param {object} options.textbox
|
|
* The container element.
|
|
*/
|
|
constructor(options = {}) {
|
|
this.textbox = options.textbox;
|
|
|
|
this.window = this.textbox.ownerGlobal;
|
|
this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window);
|
|
this.document = this.window.document;
|
|
|
|
// Create the panel to contain results.
|
|
this.textbox.appendChild(
|
|
this.window.MozXULElement.parseXULToFragment(`
|
|
<vbox class="urlbarView"
|
|
role="group"
|
|
tooltip="aHTMLTooltip">
|
|
<html:div class="urlbarView-body-outer">
|
|
<html:div class="urlbarView-body-inner">
|
|
<html:div id="urlbar-results"
|
|
class="urlbarView-results"
|
|
role="listbox"/>
|
|
</html:div>
|
|
</html:div>
|
|
<menupopup class="urlbarView-result-menu"
|
|
consumeoutsideclicks="false"/>
|
|
<hbox class="search-one-offs"
|
|
includecurrentengine="true"
|
|
disabletab="true"/>
|
|
</vbox>
|
|
`)
|
|
);
|
|
this.panel = this.textbox.querySelector(".urlbarView");
|
|
|
|
this.searchButton = lazy.UrlbarPrefs.get("experimental.searchButton");
|
|
if (this.searchButton) {
|
|
this.textbox.classList.add("searchButton");
|
|
}
|
|
|
|
this.controller = new lazy.UrlbarController({
|
|
input: this,
|
|
eventTelemetryCategory: options.eventTelemetryCategory,
|
|
});
|
|
this.view = new lazy.UrlbarView(this);
|
|
this.valueIsTyped = false;
|
|
this.formHistoryName = DEFAULT_FORM_HISTORY_NAME;
|
|
this.lastQueryContextPromise = Promise.resolve();
|
|
this._actionOverrideKeyCount = 0;
|
|
this._autofillPlaceholder = null;
|
|
this._lastSearchString = "";
|
|
this._lastValidURLStr = "";
|
|
this._valueOnLastSearch = "";
|
|
this._resultForCurrentValue = null;
|
|
this._suppressStartQuery = false;
|
|
this._suppressPrimaryAdjustment = false;
|
|
this._untrimmedValue = "";
|
|
|
|
// Search modes are per browser and are stored in this map. For a
|
|
// browser, search mode can be in preview mode, confirmed, or both.
|
|
// Typically, search mode is entered in preview mode with a particular
|
|
// source and is confirmed with the same source once a query starts. It's
|
|
// also possible for a confirmed search mode to be replaced with a preview
|
|
// mode with a different source, and in those cases, we need to re-confirm
|
|
// search mode when preview mode is exited. In addition, only confirmed
|
|
// search modes should be restored across sessions. We therefore need to
|
|
// keep track of both the current confirmed and preview modes, per browser.
|
|
//
|
|
// For each browser with a search mode, this maps the browser to an object
|
|
// like this: { preview, confirmed }. Both `preview` and `confirmed` are
|
|
// search mode objects; see the setSearchMode documentation. Either one may
|
|
// be undefined if that particular mode is not active for the browser.
|
|
this._searchModesByBrowser = new WeakMap();
|
|
|
|
this.QueryInterface = ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
]);
|
|
this._addObservers();
|
|
|
|
// This exists only for tests.
|
|
this._enableAutofillPlaceholder = true;
|
|
|
|
// Forward certain methods and properties.
|
|
const CONTAINER_METHODS = [
|
|
"getAttribute",
|
|
"hasAttribute",
|
|
"querySelector",
|
|
"setAttribute",
|
|
"removeAttribute",
|
|
"toggleAttribute",
|
|
];
|
|
const INPUT_METHODS = ["addEventListener", "blur", "removeEventListener"];
|
|
const READ_WRITE_PROPERTIES = [
|
|
"placeholder",
|
|
"readOnly",
|
|
"selectionStart",
|
|
"selectionEnd",
|
|
];
|
|
|
|
for (let method of CONTAINER_METHODS) {
|
|
this[method] = (...args) => {
|
|
return this.textbox[method](...args);
|
|
};
|
|
}
|
|
|
|
for (let method of INPUT_METHODS) {
|
|
this[method] = (...args) => {
|
|
return this.inputField[method](...args);
|
|
};
|
|
}
|
|
|
|
for (let property of READ_WRITE_PROPERTIES) {
|
|
Object.defineProperty(this, property, {
|
|
enumerable: true,
|
|
get() {
|
|
return this.inputField[property];
|
|
},
|
|
set(val) {
|
|
this.inputField[property] = val;
|
|
},
|
|
});
|
|
}
|
|
|
|
this.inputField = this.querySelector("#urlbar-input");
|
|
this._inputContainer = this.querySelector("#urlbar-input-container");
|
|
this._identityBox = this.querySelector("#identity-box");
|
|
this._searchModeIndicator = this.querySelector(
|
|
"#urlbar-search-mode-indicator"
|
|
);
|
|
this._searchModeIndicatorTitle = this._searchModeIndicator.querySelector(
|
|
"#urlbar-search-mode-indicator-title"
|
|
);
|
|
this._searchModeIndicatorClose = this._searchModeIndicator.querySelector(
|
|
"#urlbar-search-mode-indicator-close"
|
|
);
|
|
this._searchModeLabel = this.querySelector("#urlbar-label-search-mode");
|
|
this._toolbar = this.textbox.closest("toolbar");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "valueFormatter", () => {
|
|
return new lazy.UrlbarValueFormatter(this);
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "addSearchEngineHelper", () => {
|
|
return new AddSearchEngineHelper(this);
|
|
});
|
|
|
|
// If the toolbar is not visible in this window or the urlbar is readonly,
|
|
// we'll stop here, so that most properties of the input object are valid,
|
|
// but we won't handle events.
|
|
if (!this.window.toolbar.visible || this.readOnly) {
|
|
return;
|
|
}
|
|
|
|
// The event bufferer can be used to defer events that may affect users
|
|
// muscle memory; for example quickly pressing DOWN+ENTER should end up
|
|
// on a predictable result, regardless of the search status. The event
|
|
// bufferer will invoke the handling code at the right time.
|
|
this.eventBufferer = new lazy.UrlbarEventBufferer(this);
|
|
|
|
this._inputFieldEvents = [
|
|
"compositionstart",
|
|
"compositionend",
|
|
"contextmenu",
|
|
"dragover",
|
|
"dragstart",
|
|
"drop",
|
|
"focus",
|
|
"blur",
|
|
"input",
|
|
"beforeinput",
|
|
"keydown",
|
|
"keyup",
|
|
"mouseover",
|
|
"overflow",
|
|
"underflow",
|
|
"paste",
|
|
"scrollend",
|
|
"select",
|
|
];
|
|
for (let name of this._inputFieldEvents) {
|
|
this.addEventListener(name, this);
|
|
}
|
|
|
|
this.window.addEventListener("mousedown", this);
|
|
if (AppConstants.platform == "win") {
|
|
this.window.addEventListener("draggableregionleftmousedown", this);
|
|
}
|
|
this.textbox.addEventListener("mousedown", this);
|
|
|
|
// This listener handles clicks from our children too, included the search mode
|
|
// indicator close button.
|
|
this._inputContainer.addEventListener("click", this);
|
|
|
|
// This is used to detect commands launched from the panel, to avoid
|
|
// recording abandonment events when the command causes a blur event.
|
|
this.view.panel.addEventListener("command", this, true);
|
|
|
|
this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
|
|
|
|
this.window.addEventListener("customizationstarting", this);
|
|
this.window.addEventListener("aftercustomization", this);
|
|
|
|
this.updateLayoutBreakout();
|
|
|
|
this._initCopyCutController();
|
|
this._initPasteAndGo();
|
|
|
|
// Tracks IME composition.
|
|
this._compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
|
|
this._compositionClosedPopup = false;
|
|
|
|
this.editor.newlineHandling =
|
|
Ci.nsIEditor.eNewlinesStripSurroundingWhitespace;
|
|
}
|
|
|
|
/**
|
|
* Applies styling to the text in the urlbar input, depending on the text.
|
|
*/
|
|
formatValue() {
|
|
// The editor may not exist if the toolbar is not visible.
|
|
if (this.editor) {
|
|
this.valueFormatter.update();
|
|
}
|
|
}
|
|
|
|
focus() {
|
|
let beforeFocus = new CustomEvent("beforefocus", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
});
|
|
this.inputField.dispatchEvent(beforeFocus);
|
|
if (beforeFocus.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
this.inputField.focus();
|
|
}
|
|
|
|
select() {
|
|
let beforeSelect = new CustomEvent("beforeselect", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
});
|
|
this.inputField.dispatchEvent(beforeSelect);
|
|
if (beforeSelect.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
// See _on_select(). HTMLInputElement.select() dispatches a "select"
|
|
// event but does not set the primary selection.
|
|
this._suppressPrimaryAdjustment = true;
|
|
this.inputField.select();
|
|
this._suppressPrimaryAdjustment = false;
|
|
}
|
|
|
|
/**
|
|
* Sets the URI to display in the location bar.
|
|
*
|
|
* @param {nsIURI} [uri]
|
|
* If this is unspecified, the current URI will be used.
|
|
* @param {boolean} [dueToTabSwitch]
|
|
* True if this is being called due to switching tabs and false
|
|
* otherwise.
|
|
* @param {boolean} [dueToSessionRestore]
|
|
* True if this is being called due to session restore and false
|
|
* otherwise.
|
|
* @param {boolean} [dontShowSearchTerms]
|
|
* True if userTypedValue should not be overidden by search terms
|
|
* and false otherwise.
|
|
* @param {boolean} [isSameDocument]
|
|
* True if the caller of setURI loaded a new document and false
|
|
* otherwise (e.g. the location change was from an anchor scroll
|
|
* or a pushState event).
|
|
*/
|
|
setURI(
|
|
uri = null,
|
|
dueToTabSwitch = false,
|
|
dueToSessionRestore = false,
|
|
dontShowSearchTerms = false,
|
|
isSameDocument = false
|
|
) {
|
|
if (!this.window.gBrowser.userTypedValue) {
|
|
this.window.gBrowser.selectedBrowser.searchTerms = "";
|
|
if (
|
|
!dontShowSearchTerms &&
|
|
lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()
|
|
) {
|
|
this.window.gBrowser.selectedBrowser.searchTerms = lazy.UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(
|
|
this.window.gBrowser.selectedBrowser.originalURI ?? uri
|
|
);
|
|
}
|
|
}
|
|
|
|
let value = this.window.gBrowser.userTypedValue;
|
|
let valid = false;
|
|
|
|
// If `value` is null or if it's an empty string and we're switching tabs
|
|
// or the userTypedValue equals the search terms, set value to either
|
|
// search terms or the browser's current URI. When a user empties the input,
|
|
// switches tabs, and switches back, we want the URI to become visible again
|
|
// so the user knows what URI they're viewing.
|
|
// An exception to this is made in case of an auth request from a different
|
|
// base domain. To avoid auth prompt spoofing we already display the url of
|
|
// the cross domain resource, although the page is not loaded yet.
|
|
// This url will be set/unset by PromptParent. See bug 791594 for reference.
|
|
if (
|
|
value === null ||
|
|
(!value && dueToTabSwitch) ||
|
|
(value && value === this.window.gBrowser.selectedBrowser.searchTerms)
|
|
) {
|
|
if (this.window.gBrowser.selectedBrowser.searchTerms) {
|
|
value = this.window.gBrowser.selectedBrowser.searchTerms;
|
|
valid = !dueToSessionRestore;
|
|
if (!isSameDocument) {
|
|
Services.telemetry.scalarAdd(
|
|
"urlbar.persistedsearchterms.view_count",
|
|
1
|
|
);
|
|
}
|
|
} else {
|
|
uri =
|
|
this.window.gBrowser.selectedBrowser.currentAuthPromptURI ||
|
|
uri ||
|
|
(this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory
|
|
?.count === 0 &&
|
|
this.window.gBrowser.selectedBrowser.browsingContext
|
|
.nonWebControlledBlankURI) ||
|
|
this.window.gBrowser.currentURI;
|
|
// Strip off usernames and passwords for the location bar
|
|
try {
|
|
uri = Services.io.createExposableURI(uri);
|
|
} catch (e) {}
|
|
|
|
// Replace initial page URIs with an empty string
|
|
// only if there's no opener (bug 370555).
|
|
if (
|
|
this.window.isInitialPage(uri) &&
|
|
lazy.BrowserUIUtils.checkEmptyPageOrigin(
|
|
this.window.gBrowser.selectedBrowser,
|
|
uri
|
|
)
|
|
) {
|
|
value = "";
|
|
} else {
|
|
// We should deal with losslessDecodeURI throwing for exotic URIs
|
|
try {
|
|
value = losslessDecodeURI(uri);
|
|
} catch (ex) {
|
|
value = "about:blank";
|
|
}
|
|
}
|
|
// If we update the URI while restoring a session, set the proxyState to
|
|
// invalid, because we don't have a valid security state to show via site
|
|
// identity yet. See Bug 1746383.
|
|
valid =
|
|
!dueToSessionRestore &&
|
|
(!this.window.isBlankPageURL(uri.spec) ||
|
|
uri.schemeIs("moz-extension"));
|
|
}
|
|
} else if (
|
|
this.window.isInitialPage(value) &&
|
|
lazy.BrowserUIUtils.checkEmptyPageOrigin(
|
|
this.window.gBrowser.selectedBrowser
|
|
)
|
|
) {
|
|
value = "";
|
|
valid = true;
|
|
}
|
|
|
|
const previousUntrimmedValue = this.untrimmedValue;
|
|
const previousSelectionStart = this.selectionStart;
|
|
const previousSelectionEnd = this.selectionEnd;
|
|
|
|
this.value = value;
|
|
this.valueIsTyped = !valid;
|
|
this.removeAttribute("usertyping");
|
|
|
|
if (this.focused && value != previousUntrimmedValue) {
|
|
if (
|
|
previousSelectionStart != previousSelectionEnd &&
|
|
value.substring(previousSelectionStart, previousSelectionEnd) ===
|
|
previousUntrimmedValue.substring(
|
|
previousSelectionStart,
|
|
previousSelectionEnd
|
|
)
|
|
) {
|
|
// If the same text is in the same place as the previously selected text,
|
|
// the selection is kept.
|
|
this.inputField.setSelectionRange(
|
|
previousSelectionStart,
|
|
previousSelectionEnd
|
|
);
|
|
} else if (
|
|
previousSelectionEnd &&
|
|
(previousUntrimmedValue.length === previousSelectionEnd ||
|
|
value.length <= previousSelectionEnd)
|
|
) {
|
|
// If the previous end caret is not 0 and the caret is at the end of the
|
|
// input or its position is beyond the end of the new value, keep the
|
|
// position at the end.
|
|
this.inputField.setSelectionRange(value.length, value.length);
|
|
} else {
|
|
// Otherwise clear selection and set the caret position to the previous
|
|
// caret end position.
|
|
this.inputField.setSelectionRange(
|
|
previousSelectionEnd,
|
|
previousSelectionEnd
|
|
);
|
|
}
|
|
}
|
|
|
|
// The proxystate must be set before setting search mode below because
|
|
// search mode depends on it.
|
|
this.setPageProxyState(valid ? "valid" : "invalid", dueToTabSwitch);
|
|
|
|
// If we're switching tabs, restore the tab's search mode. Otherwise, if
|
|
// the URI is valid, exit search mode. This must happen after setting
|
|
// proxystate above because search mode depends on it.
|
|
if (dueToTabSwitch && !valid) {
|
|
this.restoreSearchModeState();
|
|
} else if (valid) {
|
|
this.searchMode = null;
|
|
}
|
|
|
|
// Dispatch URIUpdate event to synchronize the tab status when switching.
|
|
let event = new CustomEvent("SetURI", { bubbles: true });
|
|
this.inputField.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Converts an internal URI (e.g. a URI with a username or password) into one
|
|
* which we can expose to the user.
|
|
*
|
|
* @param {nsIURI} uri
|
|
* The URI to be converted
|
|
* @returns {nsIURI}
|
|
* The converted, exposable URI
|
|
*/
|
|
makeURIReadable(uri) {
|
|
// Avoid copying 'about:reader?url=', and always provide the original URI:
|
|
// Reader mode ensures we call createExposableURI itself.
|
|
let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay(
|
|
uri.displaySpec
|
|
);
|
|
if (readerStrippedURI) {
|
|
return readerStrippedURI;
|
|
}
|
|
|
|
try {
|
|
return Services.io.createExposableURI(uri);
|
|
} catch (ex) {}
|
|
|
|
return uri;
|
|
}
|
|
|
|
/**
|
|
* Passes DOM events to the _on_<event type> methods.
|
|
*
|
|
* @param {Event} event The event to handle.
|
|
*/
|
|
handleEvent(event) {
|
|
let methodName = "_on_" + event.type;
|
|
if (methodName in this) {
|
|
this[methodName](event);
|
|
} else {
|
|
throw new Error("Unrecognized UrlbarInput event: " + event.type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles an event which might open text or a URL. If the event requires
|
|
* doing so, handleCommand forwards it to handleNavigation.
|
|
*
|
|
* @param {Event} [event] The event triggering the open.
|
|
*/
|
|
handleCommand(event = null) {
|
|
let isMouseEvent = this.window.MouseEvent.isInstance(event);
|
|
if (isMouseEvent && event.button == 2) {
|
|
// Do nothing for right clicks.
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
if (this.view.isOpen) {
|
|
let selectedOneOff = this.view.oneOffSearchButtons.selectedButton;
|
|
if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) {
|
|
this.view.oneOffSearchButtons.handleSearchCommand(event, {
|
|
engineName: selectedOneOff.engine?.name,
|
|
source: selectedOneOff.source,
|
|
entry: "oneoff",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.handleNavigation({ event });
|
|
}
|
|
|
|
/**
|
|
* @typedef {object} HandleNavigationOneOffParams
|
|
*
|
|
* @property {string} openWhere
|
|
* Where we expect the result to be opened.
|
|
* @property {object} openParams
|
|
* The parameters related to where the result will be opened.
|
|
* @property {Node} engine
|
|
* The selected one-off's engine.
|
|
*/
|
|
|
|
/**
|
|
* Handles an event which would cause a URL or text to be opened.
|
|
*
|
|
* @param {object} [options]
|
|
* Options for the navigation.
|
|
* @param {Event} [options.event]
|
|
* The event triggering the open.
|
|
* @param {HandleNavigationOneOffParams} [options.oneOffParams]
|
|
* Optional. Pass if this navigation was triggered by a one-off. Practically
|
|
* speaking, UrlbarSearchOneOffs passes this when the user holds certain key
|
|
* modifiers while picking a one-off. In those cases, we do an immediate
|
|
* search using the one-off's engine instead of entering search mode.
|
|
* @param {object} [options.triggeringPrincipal]
|
|
* The principal that the action was triggered from.
|
|
*/
|
|
handleNavigation({ event, oneOffParams, triggeringPrincipal }) {
|
|
let element = this.view.selectedElement;
|
|
let result = this.view.getResultFromElement(element);
|
|
let openParams = oneOffParams?.openParams || {};
|
|
|
|
// If the value was submitted during composition, the result may not have
|
|
// been updated yet, because the input event happens after composition end.
|
|
// We can't trust element nor _resultForCurrentValue targets in that case,
|
|
// so we always generate a new heuristic to load.
|
|
let isComposing = this.editor.composing;
|
|
|
|
// Use the selected element if we have one; this is usually the case
|
|
// when the view is open.
|
|
let selectedPrivateResult =
|
|
result &&
|
|
result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
|
|
result.payload.inPrivateWindow;
|
|
let selectedPrivateEngineResult =
|
|
selectedPrivateResult && result.payload.isPrivateEngine;
|
|
if (
|
|
!isComposing &&
|
|
element &&
|
|
(!oneOffParams?.engine || selectedPrivateEngineResult)
|
|
) {
|
|
this.pickElement(element, event);
|
|
return;
|
|
}
|
|
|
|
// Use the hidden heuristic if it exists and there's no selection.
|
|
if (
|
|
lazy.UrlbarPrefs.get("experimental.hideHeuristic") &&
|
|
!element &&
|
|
!isComposing &&
|
|
!oneOffParams?.engine &&
|
|
this._resultForCurrentValue?.heuristic
|
|
) {
|
|
this.pickResult(this._resultForCurrentValue, event);
|
|
return;
|
|
}
|
|
|
|
// We don't select a heuristic result when we're autofilling a token alias,
|
|
// but we want pressing Enter to behave like the first result was selected.
|
|
if (!result && this.value.startsWith("@")) {
|
|
let tokenAliasResult = this.view.getResultAtIndex(0);
|
|
if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
|
|
this.pickResult(tokenAliasResult, event);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let url;
|
|
let selType = this.controller.engagementEvent.typeFromElement(
|
|
result,
|
|
element
|
|
);
|
|
let typedValue = this.value;
|
|
if (oneOffParams?.engine) {
|
|
selType = "oneoff";
|
|
typedValue = this._lastSearchString;
|
|
// If there's a selected one-off button then load a search using
|
|
// the button's engine.
|
|
result = this._resultForCurrentValue;
|
|
|
|
let searchString =
|
|
(result && (result.payload.suggestion || result.payload.query)) ||
|
|
this._lastSearchString;
|
|
[url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl(
|
|
oneOffParams.engine,
|
|
searchString
|
|
);
|
|
this._recordSearch(oneOffParams.engine, event, { url });
|
|
|
|
lazy.UrlbarUtils.addToFormHistory(
|
|
this,
|
|
searchString,
|
|
oneOffParams.engine.name
|
|
).catch(console.error);
|
|
} else {
|
|
// Use the current value if we don't have a UrlbarResult e.g. because the
|
|
// view is closed.
|
|
url = this.untrimmedValue;
|
|
openParams.postData = null;
|
|
}
|
|
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
// When the user hits enter in a local search mode and there's no selected
|
|
// result or one-off, don't do anything.
|
|
if (
|
|
this.searchMode &&
|
|
!this.searchMode.engineName &&
|
|
!result &&
|
|
!oneOffParams
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let selectedResult = result || this.view.selectedResult;
|
|
this.controller.recordSelectedResult(event, selectedResult);
|
|
|
|
let where = oneOffParams?.openWhere || this._whereToOpen(event);
|
|
if (selectedPrivateResult) {
|
|
where = "window";
|
|
openParams.private = true;
|
|
}
|
|
openParams.allowInheritPrincipal = false;
|
|
url = this._maybeCanonizeURL(event, url) || url.trim();
|
|
|
|
this.controller.engagementEvent.record(event, {
|
|
element,
|
|
selType,
|
|
searchString: typedValue,
|
|
result: selectedResult,
|
|
});
|
|
|
|
let isValidUrl = false;
|
|
try {
|
|
new URL(url);
|
|
isValidUrl = true;
|
|
} catch (ex) {}
|
|
if (isValidUrl) {
|
|
this._loadURL(url, event, where, openParams);
|
|
return;
|
|
}
|
|
|
|
// This is not a URL and there's no selected element, because likely the
|
|
// view is closed, or paste&go was used.
|
|
// We must act consistently here, having or not an open view should not
|
|
// make a difference if the search string is the same.
|
|
|
|
// If we have a result for the current value, we can just use it.
|
|
if (!isComposing && this._resultForCurrentValue) {
|
|
this.pickResult(this._resultForCurrentValue, event);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, we must fetch the heuristic result for the current value.
|
|
// TODO (Bug 1604927): If the urlbar results are restricted to a specific
|
|
// engine, here we must search with that specific engine; indeed the
|
|
// docshell wouldn't know about our engine restriction.
|
|
// Also remember to invoke this._recordSearch, after replacing url with
|
|
// the appropriate engine submission url.
|
|
let browser = this.window.gBrowser.selectedBrowser;
|
|
let lastLocationChange = browser.lastLocationChange;
|
|
lazy.UrlbarUtils.getHeuristicResultFor(url)
|
|
.then(newResult => {
|
|
// Because this happens asynchronously, we must verify that the browser
|
|
// location did not change in the meanwhile.
|
|
if (
|
|
where != "current" ||
|
|
browser.lastLocationChange == lastLocationChange
|
|
) {
|
|
this.pickResult(newResult, event, null, browser);
|
|
}
|
|
})
|
|
.catch(ex => {
|
|
if (url) {
|
|
// Something went wrong, we should always have a heuristic result,
|
|
// otherwise it means we're not able to search at all, maybe because
|
|
// some parts of the profile are corrupt.
|
|
// The urlbar should still allow to search or visit the typed string,
|
|
// so that the user can look for help to resolve the problem.
|
|
let flags =
|
|
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
|
|
Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
|
|
if (this.isPrivate) {
|
|
flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
|
|
}
|
|
let {
|
|
preferredURI: uri,
|
|
postData,
|
|
} = Services.uriFixup.getFixupURIInfo(url, flags);
|
|
if (
|
|
where != "current" ||
|
|
browser.lastLocationChange == lastLocationChange
|
|
) {
|
|
openParams.postData = postData;
|
|
this._loadURL(uri.spec, event, where, openParams, null, browser);
|
|
}
|
|
}
|
|
});
|
|
// Don't add further handling here, the catch above is our last resort.
|
|
}
|
|
|
|
handleRevert(dontShowSearchTerms = false) {
|
|
this.window.gBrowser.userTypedValue = null;
|
|
// Nullify search mode before setURI so it won't try to restore it.
|
|
this.searchMode = null;
|
|
this.setURI(null, true, false, dontShowSearchTerms);
|
|
if (this.value && this.focused) {
|
|
this.select();
|
|
}
|
|
}
|
|
|
|
maybeHandleRevertFromPopup(anchorElement) {
|
|
if (
|
|
anchorElement?.closest("#urlbar") &&
|
|
this.window.gBrowser.selectedBrowser.searchTerms
|
|
) {
|
|
// The Persist Search Tip can be open while a PopupNotification is queued
|
|
// to appear, so ensure that the tip is closed.
|
|
this.view.close();
|
|
|
|
this.handleRevert(true);
|
|
Services.telemetry.scalarAdd(
|
|
"urlbar.persistedsearchterms.revert_by_popup_count",
|
|
1
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by inputs that resemble search boxes, but actually hand input off
|
|
* to the Urlbar. We use these fake inputs on the new tab page and
|
|
* about:privatebrowsing.
|
|
*
|
|
* @param {string} searchString
|
|
* The search string to use.
|
|
* @param {nsISearchEngine} [searchEngine]
|
|
* Optional. If included and the right prefs are set, we will enter search
|
|
* mode when handing `searchString` from the fake input to the Urlbar.
|
|
* @param {string} newtabSessionId
|
|
* Optional. The id of the newtab session that handed off this search.
|
|
*
|
|
*/
|
|
handoff(searchString, searchEngine, newtabSessionId) {
|
|
this._isHandoffSession = true;
|
|
this._handoffSession = newtabSessionId;
|
|
if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) {
|
|
this.search(searchString, {
|
|
searchEngine,
|
|
searchModeEntry: "handoff",
|
|
});
|
|
} else {
|
|
this.search(searchString);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when an element of the view is picked.
|
|
*
|
|
* @param {Element} element The element that was picked.
|
|
* @param {Event} event The event that picked the element.
|
|
*/
|
|
pickElement(element, event) {
|
|
let result = this.view.getResultFromElement(element);
|
|
if (!result) {
|
|
return;
|
|
}
|
|
this.pickResult(result, event, element);
|
|
}
|
|
|
|
/**
|
|
* Called when a result is picked.
|
|
*
|
|
* @param {UrlbarResult} result The result that was picked.
|
|
* @param {Event} event The event that picked the result.
|
|
* @param {DOMElement} element the picked view element, if available.
|
|
* @param {object} browser The browser to use for the load.
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
pickResult(
|
|
result,
|
|
event,
|
|
element = null,
|
|
browser = this.window.gBrowser.selectedBrowser
|
|
) {
|
|
if (element?.classList.contains("urlbarView-button-menu")) {
|
|
this.view.openResultMenu(result, element);
|
|
return;
|
|
}
|
|
|
|
let urlOverride;
|
|
if (element?.dataset.command) {
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
searchString: this._lastSearchString,
|
|
selType:
|
|
element.dataset.command == "help" &&
|
|
result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP
|
|
? "tiphelp"
|
|
: element.dataset.command,
|
|
});
|
|
if (element.dataset.command == "help") {
|
|
urlOverride = result.payload.helpUrl;
|
|
}
|
|
urlOverride ||= element.dataset.url;
|
|
if (!urlOverride) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// When a one-off is selected, we restyle heuristic results to look like
|
|
// search results. In the unlikely event that they are clicked, instead of
|
|
// picking the results as usual, we confirm search mode, same as if the user
|
|
// had selected them and pressed the enter key. Restyling results in this
|
|
// manner was agreed on as a compromise between consistent UX and
|
|
// engineering effort. See review discussion at bug 1667766.
|
|
if (
|
|
result.heuristic &&
|
|
this.searchMode?.isPreview &&
|
|
this.view.oneOffSearchButtons.selectedButton
|
|
) {
|
|
this.confirmSearchMode();
|
|
this.search(this.value);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP &&
|
|
result.payload.type == "dismissalAcknowledgment"
|
|
) {
|
|
// The user clicked the "Got it" button inside the dismissal
|
|
// acknowledgment tip. Dismiss the tip.
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
searchString: this._lastSearchString,
|
|
selType: "dismiss",
|
|
});
|
|
this.view.onQueryResultRemoved(result.rowIndex);
|
|
return;
|
|
}
|
|
|
|
urlOverride ||= element?.dataset.url;
|
|
let originalUntrimmedValue = this.untrimmedValue;
|
|
let isCanonized = this.setValueFromResult({ result, event, urlOverride });
|
|
let where = this._whereToOpen(event);
|
|
let openParams = {
|
|
allowInheritPrincipal: false,
|
|
globalHistoryOptions: {
|
|
triggeringSearchEngine: result.payload?.engine,
|
|
triggeringSponsoredURL: result.payload?.isSponsored
|
|
? result.payload.url
|
|
: undefined,
|
|
},
|
|
private: this.isPrivate,
|
|
};
|
|
|
|
if (
|
|
urlOverride &&
|
|
result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP &&
|
|
where == "current"
|
|
) {
|
|
// Open non-tip help links in a new tab unless the user held a modifier.
|
|
// TODO (bug 1696232): Do this for tip help links, too.
|
|
where = "tab";
|
|
}
|
|
|
|
if (!result.payload.providesSearchMode) {
|
|
this.view.close({ elementPicked: true });
|
|
}
|
|
|
|
this.controller.recordSelectedResult(event, result);
|
|
|
|
if (isCanonized) {
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
selType: "canonized",
|
|
searchString: this._lastSearchString,
|
|
});
|
|
this._loadURL(this.value, event, where, openParams, browser);
|
|
return;
|
|
}
|
|
|
|
let { url, postData } = urlOverride
|
|
? { url: urlOverride, postData: null }
|
|
: lazy.UrlbarUtils.getUrlFromResult(result);
|
|
openParams.postData = postData;
|
|
|
|
switch (result.type) {
|
|
case lazy.UrlbarUtils.RESULT_TYPE.URL: {
|
|
// Bug 1578856: both the provider and the docshell run heuristics to
|
|
// decide how to handle a non-url string, either fixing it to a url, or
|
|
// searching for it.
|
|
// Some preferences can control the docshell behavior, for example
|
|
// if dns_first_for_single_words is true, the docshell looks up the word
|
|
// against the dns server, and either loads it as an url or searches for
|
|
// it, depending on the lookup result. The provider instead will always
|
|
// return a fixed url in this case, because URIFixup is synchronous and
|
|
// can't do a synchronous dns lookup. A possible long term solution
|
|
// would involve sharing the docshell logic with the provider, along
|
|
// with the dns lookup.
|
|
// For now, in this specific case, we'll override the result's url
|
|
// with the input value, and let it pass through to _loadURL(), and
|
|
// finally to the docshell.
|
|
// This also means that in some cases the heuristic result will show a
|
|
// Visit entry, but the docshell will instead execute a search. It's a
|
|
// rare case anyway, most likely to happen for enterprises customizing
|
|
// the urifixup prefs.
|
|
if (
|
|
result.heuristic &&
|
|
lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
|
|
lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
|
|
) {
|
|
url = originalUntrimmedValue;
|
|
}
|
|
break;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: {
|
|
// If this result comes from a bookmark keyword, let it inherit the
|
|
// current document's principal, otherwise bookmarklets would break.
|
|
openParams.allowInheritPrincipal = true;
|
|
break;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
|
|
if (this.hasAttribute("actionoverride")) {
|
|
where = "current";
|
|
break;
|
|
}
|
|
|
|
// Keep the searchMode for telemetry since handleRevert sets it to null.
|
|
const searchMode = this.searchMode;
|
|
this.handleRevert();
|
|
let prevTab = this.window.gBrowser.selectedTab;
|
|
let loadOpts = {
|
|
adoptIntoActiveWindow: lazy.UrlbarPrefs.get(
|
|
"switchTabs.adoptIntoActiveWindow"
|
|
),
|
|
};
|
|
|
|
// We cache the search string because switching tab may clear it.
|
|
let searchString = this._lastSearchString;
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
searchString,
|
|
searchMode,
|
|
selType: "tabswitch",
|
|
});
|
|
|
|
let switched = this.window.switchToTabHavingURI(
|
|
Services.io.newURI(url),
|
|
false,
|
|
loadOpts
|
|
);
|
|
if (switched && prevTab.isEmpty) {
|
|
this.window.gBrowser.removeTab(prevTab);
|
|
}
|
|
|
|
if (switched && !this.isPrivate && !result.heuristic) {
|
|
// We don't await for this, because a rejection should not interrupt
|
|
// the load. Just reportError it.
|
|
lazy.UrlbarUtils.addToInputHistory(url, searchString).catch(
|
|
console.error
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
|
|
if (result.payload.providesSearchMode) {
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
searchString: this._lastSearchString,
|
|
selType: this.controller.engagementEvent.typeFromElement(
|
|
result,
|
|
element
|
|
),
|
|
});
|
|
this.maybeConfirmSearchModeFromResult({
|
|
result,
|
|
checkValue: false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!this.searchMode &&
|
|
result.heuristic &&
|
|
// If we asked the DNS earlier, avoid the post-facto check.
|
|
!lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
|
|
// TODO (bug 1642623): for now there is no smart heuristic to skip the
|
|
// DNS lookup, so any value above 0 will run it.
|
|
lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 &&
|
|
this.window.gKeywordURIFixup &&
|
|
lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
|
|
) {
|
|
// When fixing a single word to a search, the docShell would also
|
|
// query the DNS and if resolved ask the user whether they would
|
|
// rather visit that as a host. On a positive answer, it adds the host
|
|
// to the list that we use to make decisions.
|
|
// Because we are directly asking for a search here, bypassing the
|
|
// docShell, we need to do the same ourselves.
|
|
// See also URIFixupChild.jsm and keyword-uri-fixup.
|
|
let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim());
|
|
if (fixupInfo) {
|
|
this.window.gKeywordURIFixup.check(
|
|
this.window.gBrowser.selectedBrowser,
|
|
fixupInfo
|
|
);
|
|
}
|
|
}
|
|
|
|
if (result.payload.inPrivateWindow) {
|
|
where = "window";
|
|
openParams.private = true;
|
|
}
|
|
|
|
const actionDetails = {
|
|
isSuggestion: !!result.payload.suggestion,
|
|
isFormHistory:
|
|
result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY,
|
|
alias: result.payload.keyword,
|
|
url,
|
|
};
|
|
const engine = Services.search.getEngineByName(result.payload.engine);
|
|
this._recordSearch(engine, event, actionDetails);
|
|
|
|
if (!result.payload.inPrivateWindow) {
|
|
lazy.UrlbarUtils.addToFormHistory(
|
|
this,
|
|
result.payload.suggestion || result.payload.query,
|
|
engine.name
|
|
).catch(console.error);
|
|
}
|
|
break;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
|
|
let scalarName =
|
|
element.classList.contains("urlbarView-button-help") ||
|
|
element.dataset.command == "help"
|
|
? `${result.payload.type}-help`
|
|
: `${result.payload.type}-picked`;
|
|
Services.telemetry.keyedScalarAdd("urlbar.tips", scalarName, 1);
|
|
if (url) {
|
|
break;
|
|
}
|
|
this.handleRevert();
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
selType: "tip",
|
|
searchString: this._lastSearchString,
|
|
});
|
|
return;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: {
|
|
if (url) {
|
|
break;
|
|
}
|
|
url = result.payload.url;
|
|
// Keep the searchMode for telemetry since handleRevert sets it to null.
|
|
const searchMode = this.searchMode;
|
|
// Do not revert the Urlbar if we're going to navigate. We want the URL
|
|
// populated so we can navigate to it.
|
|
if (!url || !result.payload.shouldNavigate) {
|
|
this.handleRevert();
|
|
}
|
|
// If we won't be navigating, this is the end of the engagement.
|
|
if (!url || !result.payload.shouldNavigate) {
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
searchMode,
|
|
searchString: this._lastSearchString,
|
|
selType: this.controller.engagementEvent.typeFromElement(
|
|
result,
|
|
element
|
|
),
|
|
});
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: {
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
selType: "extension",
|
|
searchString: this._lastSearchString,
|
|
});
|
|
|
|
// The urlbar needs to revert to the loaded url when a command is
|
|
// handled by the extension.
|
|
this.handleRevert();
|
|
// We don't directly handle a load when an Omnibox API result is picked,
|
|
// instead we forward the request to the WebExtension itself, because
|
|
// the value may not even be a url.
|
|
// We pass the keyword and content, that actually is the retrieved value
|
|
// prefixed by the keyword. ExtensionSearchHandler uses this keyword
|
|
// redundancy as a sanity check.
|
|
lazy.ExtensionSearchHandler.handleInputEntered(
|
|
result.payload.keyword,
|
|
result.payload.content,
|
|
where
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!url) {
|
|
throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
|
|
}
|
|
|
|
// Record input history but only in non-private windows.
|
|
if (!this.isPrivate) {
|
|
let input;
|
|
if (!result.heuristic) {
|
|
input = this._lastSearchString;
|
|
} else if (result.autofill?.type == "adaptive") {
|
|
input = result.autofill.adaptiveHistoryInput;
|
|
}
|
|
// `input` may be an empty string, so do a strict comparison here.
|
|
if (input !== undefined) {
|
|
// We don't await for this, because a rejection should not interrupt
|
|
// the load. Just reportError it.
|
|
lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error);
|
|
}
|
|
}
|
|
|
|
this.controller.engagementEvent.record(event, {
|
|
result,
|
|
element,
|
|
searchString: this._lastSearchString,
|
|
selType: this.controller.engagementEvent.typeFromElement(result, element),
|
|
searchSource: this.getSearchSource(event),
|
|
});
|
|
|
|
if (result.payload.sendAttributionRequest) {
|
|
lazy.PartnerLinkAttribution.makeRequest({
|
|
targetURL: result.payload.url,
|
|
source: "urlbar",
|
|
campaignID: Services.prefs.getStringPref(
|
|
"browser.partnerlink.campaign.topsites"
|
|
),
|
|
});
|
|
if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") {
|
|
// The position is 1-based for telemetry
|
|
const position = result.rowIndex + 1;
|
|
Services.telemetry.keyedScalarAdd(
|
|
SCALAR_CATEGORY_TOPSITES,
|
|
`urlbar_${position}`,
|
|
1
|
|
);
|
|
lazy.PartnerLinkAttribution.sendContextualServicesPing(
|
|
{
|
|
position,
|
|
source: "urlbar",
|
|
tile_id: result.payload.sponsoredTileId || -1,
|
|
reporting_url: result.payload.sponsoredClickUrl,
|
|
advertiser: result.payload.title.toLocaleLowerCase(),
|
|
},
|
|
lazy.CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_SELECTION
|
|
);
|
|
}
|
|
}
|
|
|
|
this._loadURL(
|
|
url,
|
|
event,
|
|
where,
|
|
openParams,
|
|
{
|
|
source: result.source,
|
|
type: result.type,
|
|
searchTerm: result.payload.suggestion ?? result.payload.query,
|
|
},
|
|
browser
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Called by the view when moving through results with the keyboard, and when
|
|
* picking a result. This sets the input value to the value of the result and
|
|
* invalidates the pageproxystate. It also sets the result that is associated
|
|
* with the current input value. If you need to set this result but don't
|
|
* want to also set the input value, then use setResultForCurrentValue.
|
|
*
|
|
* @param {object} options
|
|
* Options.
|
|
* @param {UrlbarResult} [options.result]
|
|
* The result that was selected or picked, null if no result was selected.
|
|
* @param {Event} [options.event]
|
|
* The event that picked the result.
|
|
* @param {string} [options.urlOverride]
|
|
* Normally the URL is taken from `result.payload.url`, but if `urlOverride`
|
|
* is specified, it's used instead.
|
|
* @returns {boolean}
|
|
* Whether the value has been canonized
|
|
*/
|
|
setValueFromResult({ result = null, event = null, urlOverride = null } = {}) {
|
|
// Usually this is set by a previous input event, but in certain cases, like
|
|
// when opening Top Sites on a loaded page, it wouldn't happen. To avoid
|
|
// confusing the user, we always enforce it when a result changes our value.
|
|
this.setPageProxyState("invalid", true);
|
|
|
|
// A previous result may have previewed search mode. If we don't expect that
|
|
// we might stay in a search mode of some kind, exit it now.
|
|
if (
|
|
this.searchMode?.isPreview &&
|
|
!result?.payload.providesSearchMode &&
|
|
!this.view.oneOffSearchButtons.selectedButton
|
|
) {
|
|
this.searchMode = null;
|
|
}
|
|
|
|
if (!result) {
|
|
// This happens when there's no selection, for example when moving to the
|
|
// one-offs search settings button, or to the input field when Top Sites
|
|
// are shown; then we must reset the input value.
|
|
// Note that for Top Sites the last search string would be empty, thus we
|
|
// must restore the last text value.
|
|
// Note that unselected autofill results will still arrive in this
|
|
// function with a non-null `result`. They are handled below.
|
|
this.value = this._lastSearchString || this._valueOnLastSearch;
|
|
this.setResultForCurrentValue(result);
|
|
return false;
|
|
}
|
|
|
|
// The value setter clobbers the actiontype attribute, so we need this
|
|
// helper to restore it afterwards.
|
|
const setValueAndRestoreActionType = (value, allowTrim) => {
|
|
this._setValue(value, allowTrim);
|
|
|
|
switch (result.type) {
|
|
case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
|
|
this.setAttribute("actiontype", "switchtab");
|
|
break;
|
|
case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
|
|
this.setAttribute("actiontype", "extension");
|
|
break;
|
|
}
|
|
};
|
|
|
|
// For autofilled results, the value that should be canonized is not the
|
|
// autofilled value but the value that the user typed.
|
|
let canonizedUrl = this._maybeCanonizeURL(
|
|
event,
|
|
result.autofill ? this._lastSearchString : this.value
|
|
);
|
|
if (canonizedUrl) {
|
|
setValueAndRestoreActionType(canonizedUrl, true);
|
|
this.setResultForCurrentValue(result);
|
|
return true;
|
|
}
|
|
|
|
if (result.autofill) {
|
|
this._autofillValue(result.autofill);
|
|
}
|
|
|
|
if (result.payload.providesSearchMode) {
|
|
let enteredSearchMode;
|
|
// Only preview search mode if the result is selected.
|
|
if (this.view.resultIsSelected(result)) {
|
|
// Not starting a query means we will only preview search mode.
|
|
enteredSearchMode = this.maybeConfirmSearchModeFromResult({
|
|
result,
|
|
checkValue: false,
|
|
startQuery: false,
|
|
});
|
|
}
|
|
if (!enteredSearchMode) {
|
|
setValueAndRestoreActionType(this._getValueFromResult(result), true);
|
|
this.searchMode = null;
|
|
}
|
|
this.setResultForCurrentValue(result);
|
|
return false;
|
|
}
|
|
|
|
// If the url is trimmed but it's invalid (for example it has an unknown
|
|
// single word host, or an unknown domain suffix), trimming
|
|
// it would end up executing a search instead of visiting it.
|
|
let allowTrim = true;
|
|
if (
|
|
(urlOverride || result.type == lazy.UrlbarUtils.RESULT_TYPE.URL) &&
|
|
lazy.UrlbarPrefs.get("trimURLs")
|
|
) {
|
|
let url = urlOverride || result.payload.url;
|
|
if (url.startsWith(lazy.BrowserUIUtils.trimURLProtocol)) {
|
|
let fixupInfo = this._getURIFixupInfo(lazy.BrowserUIUtils.trimURL(url));
|
|
if (fixupInfo?.keywordAsSent) {
|
|
allowTrim = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!result.autofill) {
|
|
setValueAndRestoreActionType(
|
|
this._getValueFromResult(result, urlOverride),
|
|
allowTrim
|
|
);
|
|
}
|
|
|
|
this.setResultForCurrentValue(result);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* The input keeps track of the result associated with the current input
|
|
* value. This result can be set by calling either setValueFromResult or this
|
|
* method. Use this method when you need to set the result without also
|
|
* setting the input value. This can be the case when either the selection is
|
|
* cleared and no other result becomes selected, or when the result is the
|
|
* heuristic and we don't want to modify the value the user is typing.
|
|
*
|
|
* @param {UrlbarResult} result
|
|
* The result to associate with the current input value.
|
|
*/
|
|
setResultForCurrentValue(result) {
|
|
this._resultForCurrentValue = result;
|
|
}
|
|
|
|
/**
|
|
* Called by the controller when the first result of a new search is received.
|
|
* If it's an autofill result, then it may need to be autofilled, subject to a
|
|
* few restrictions.
|
|
*
|
|
* @param {UrlbarResult} result
|
|
* The first result.
|
|
*/
|
|
_autofillFirstResult(result) {
|
|
if (!result.autofill) {
|
|
return;
|
|
}
|
|
|
|
let isPlaceholderSelected =
|
|
this._autofillPlaceholder &&
|
|
this.selectionEnd == this._autofillPlaceholder.value.length &&
|
|
this.selectionStart == this._lastSearchString.length &&
|
|
this._autofillPlaceholder.value
|
|
.toLocaleLowerCase()
|
|
.startsWith(this._lastSearchString.toLocaleLowerCase());
|
|
|
|
// Don't autofill if there's already a selection (with one caveat described
|
|
// next) or the cursor isn't at the end of the input. But if there is a
|
|
// selection and it's the autofill placeholder value, then do autofill.
|
|
if (
|
|
!isPlaceholderSelected &&
|
|
!this._autofillIgnoresSelection &&
|
|
(this.selectionStart != this.selectionEnd ||
|
|
this.selectionEnd != this._lastSearchString.length)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.setValueFromResult({ result });
|
|
}
|
|
|
|
/**
|
|
* Invoked by the controller when the first result is received.
|
|
*
|
|
* @param {UrlbarResult} firstResult
|
|
* The first result received.
|
|
* @returns {boolean}
|
|
* True if this method canceled the query and started a new one. False
|
|
* otherwise.
|
|
*/
|
|
onFirstResult(firstResult) {
|
|
// If the heuristic result has a keyword but isn't a keyword offer, we may
|
|
// need to enter search mode.
|
|
if (
|
|
firstResult.heuristic &&
|
|
firstResult.payload.keyword &&
|
|
!firstResult.payload.providesSearchMode &&
|
|
this.maybeConfirmSearchModeFromResult({
|
|
result: firstResult,
|
|
entry: "typed",
|
|
checkValue: false,
|
|
})
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// To prevent selection flickering, we apply autofill on input through a
|
|
// placeholder, without waiting for results. But, if the first result is
|
|
// not an autofill one, the autofill prediction was wrong and we should
|
|
// restore the original user typed string.
|
|
if (firstResult.autofill) {
|
|
this._autofillFirstResult(firstResult);
|
|
} else if (
|
|
this._autofillPlaceholder &&
|
|
// Avoid clobbering added spaces (for token aliases, for example).
|
|
!this.value.endsWith(" ")
|
|
) {
|
|
this._autofillPlaceholder = null;
|
|
this._setValue(this.window.gBrowser.userTypedValue, false);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Starts a query based on the current input value.
|
|
*
|
|
* @param {object} [options]
|
|
* Object options
|
|
* @param {boolean} [options.allowAutofill]
|
|
* Whether or not to allow providers to include autofill results.
|
|
* @param {boolean} [options.autofillIgnoresSelection]
|
|
* Normally we autofill only if the cursor is at the end of the string,
|
|
* if this is set we'll autofill regardless of selection.
|
|
* @param {string} [options.searchString]
|
|
* The search string. If not given, the current input value is used.
|
|
* Otherwise, the current input value must start with this value.
|
|
* @param {boolean} [options.resetSearchState]
|
|
* If this is the first search of a user interaction with the input, set
|
|
* this to true (the default) so that search-related state from the previous
|
|
* interaction doesn't interfere with the new interaction. Otherwise set it
|
|
* to false so that state is maintained during a single interaction. The
|
|
* intended use for this parameter is that it should be set to false when
|
|
* this method is called due to input events.
|
|
* @param {event} [options.event]
|
|
* The user-generated event that triggered the query, if any. If given, we
|
|
* will record engagement event telemetry for the query.
|
|
*/
|
|
startQuery({
|
|
allowAutofill = true,
|
|
autofillIgnoresSelection = false,
|
|
searchString = null,
|
|
resetSearchState = true,
|
|
event = null,
|
|
} = {}) {
|
|
if (!searchString) {
|
|
searchString =
|
|
this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
|
|
} else if (!this.value.startsWith(searchString)) {
|
|
throw new Error("The current value doesn't start with the search string");
|
|
}
|
|
|
|
if (event) {
|
|
this.controller.engagementEvent.start(event, searchString);
|
|
}
|
|
|
|
if (this._suppressStartQuery) {
|
|
return;
|
|
}
|
|
|
|
this._autofillIgnoresSelection = autofillIgnoresSelection;
|
|
if (resetSearchState) {
|
|
this._resetSearchState();
|
|
}
|
|
|
|
this._lastSearchString = searchString;
|
|
this._valueOnLastSearch = this.value;
|
|
|
|
let options = {
|
|
allowAutofill,
|
|
isPrivate: this.isPrivate,
|
|
maxResults: lazy.UrlbarPrefs.get("maxRichResults"),
|
|
searchString,
|
|
view: this.view,
|
|
userContextId: this.window.gBrowser.selectedBrowser.getAttribute(
|
|
"usercontextid"
|
|
),
|
|
currentPage: this.window.gBrowser.currentURI.spec,
|
|
formHistoryName: this.formHistoryName,
|
|
prohibitRemoteResults:
|
|
event &&
|
|
lazy.UrlbarUtils.isPasteEvent(event) &&
|
|
lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") <
|
|
event.data?.length,
|
|
};
|
|
|
|
if (this.searchMode) {
|
|
this.confirmSearchMode();
|
|
options.searchMode = this.searchMode;
|
|
if (this.searchMode.source) {
|
|
options.sources = [this.searchMode.source];
|
|
}
|
|
}
|
|
|
|
// TODO (Bug 1522902): This promise is necessary for tests, because some
|
|
// tests are not listening for completion when starting a query through
|
|
// other methods than startQuery (input events for example).
|
|
this.lastQueryContextPromise = this.controller.startQuery(
|
|
new lazy.UrlbarQueryContext(options)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sets the input's value, starts a search, and opens the view.
|
|
*
|
|
* @param {string} value
|
|
* The input's value will be set to this value, and the search will
|
|
* use it as its query.
|
|
* @param {object} [options]
|
|
* Object options
|
|
* @param {nsISearchEngine} [options.searchEngine]
|
|
* Search engine to use when the search is using a known alias.
|
|
* @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry]
|
|
* If provided, we will record this parameter as the search mode entry point
|
|
* in Telemetry. Consumers should provide this if they expect their call
|
|
* to enter search mode.
|
|
* @param {boolean} [options.focus]
|
|
* If true, the urlbar will be focused. If false, the focus will remain
|
|
* unchanged.
|
|
*/
|
|
search(value, { searchEngine, searchModeEntry, focus = true } = {}) {
|
|
if (focus) {
|
|
this.focus();
|
|
}
|
|
let trimmedValue = value.trim();
|
|
let end = trimmedValue.search(lazy.UrlbarTokenizer.REGEXP_SPACES);
|
|
let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end);
|
|
// Enter search mode if the string starts with a restriction token.
|
|
let searchMode = lazy.UrlbarUtils.searchModeForToken(firstToken);
|
|
let firstTokenIsRestriction = !!searchMode;
|
|
if (!searchMode && searchEngine) {
|
|
searchMode = { engineName: searchEngine.name };
|
|
firstTokenIsRestriction = searchEngine.aliases.includes(firstToken);
|
|
}
|
|
|
|
if (searchMode) {
|
|
searchMode.entry = searchModeEntry;
|
|
this.searchMode = searchMode;
|
|
if (firstTokenIsRestriction) {
|
|
// Remove the restriction token/alias from the string to be searched for
|
|
// in search mode.
|
|
value = value.replace(firstToken, "");
|
|
}
|
|
if (lazy.UrlbarTokenizer.REGEXP_SPACES.test(value[0])) {
|
|
// If there was a trailing space after the restriction token/alias,
|
|
// remove it.
|
|
value = value.slice(1);
|
|
}
|
|
this._revertOnBlurValue = value;
|
|
} else if (
|
|
Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken)
|
|
) {
|
|
this.searchMode = null;
|
|
// If the entire value is a restricted token, append a space.
|
|
if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) {
|
|
value += " ";
|
|
}
|
|
this._revertOnBlurValue = value;
|
|
}
|
|
this.inputField.value = value;
|
|
// Avoid selecting the text if this method is called twice in a row.
|
|
this.selectionStart = -1;
|
|
|
|
// Note: proper IME Composition handling depends on the fact this generates
|
|
// an input event, rather than directly invoking the controller; everything
|
|
// goes through _on_input, that will properly skip the search until the
|
|
// composition is committed. _on_input also skips the search when it's the
|
|
// same as the previous search, but we want to allow consecutive searches
|
|
// with the same string. So clear _lastSearchString first.
|
|
this._lastSearchString = "";
|
|
let event = new UIEvent("input", {
|
|
bubbles: true,
|
|
cancelable: false,
|
|
view: this.window,
|
|
detail: 0,
|
|
});
|
|
this.inputField.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Focus without the focus styles.
|
|
* This is used by Activity Stream and about:privatebrowsing for search hand-off.
|
|
*/
|
|
setHiddenFocus() {
|
|
this._hideFocus = true;
|
|
if (this.focused) {
|
|
this.removeAttribute("focused");
|
|
} else {
|
|
this.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore focus styles.
|
|
* This is used by Activity Stream and about:privatebrowsing for search hand-off.
|
|
*
|
|
* @param {Browser} forceSuppressFocusBorder
|
|
* Set true to suppress-focus-border attribute if this flag is true.
|
|
*/
|
|
removeHiddenFocus(forceSuppressFocusBorder = false) {
|
|
this._hideFocus = false;
|
|
if (this.focused) {
|
|
this.setAttribute("focused", "true");
|
|
|
|
if (forceSuppressFocusBorder) {
|
|
this.toggleAttribute("suppress-focus-border", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the search mode for a specific browser instance.
|
|
*
|
|
* @param {Browser} browser
|
|
* The search mode for this browser will be returned.
|
|
* @param {boolean} [confirmedOnly]
|
|
* Normally, if the browser has both preview and confirmed modes, preview
|
|
* mode will be returned since it takes precedence. If this argument is
|
|
* true, then only confirmed search mode will be returned, or null if
|
|
* search mode hasn't been confirmed.
|
|
* @returns {object}
|
|
* A search mode object. See setSearchMode documentation. If the browser
|
|
* is not in search mode, then null is returned.
|
|
*/
|
|
getSearchMode(browser, confirmedOnly = false) {
|
|
let modes = this._searchModesByBrowser.get(browser);
|
|
|
|
// Return copies so that callers don't modify the stored values.
|
|
if (!confirmedOnly && modes?.preview) {
|
|
return { ...modes.preview };
|
|
}
|
|
if (modes?.confirmed) {
|
|
return { ...modes.confirmed };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets search mode for a specific browser instance. If the given browser is
|
|
* selected, then this will also enter search mode.
|
|
*
|
|
* @param {object} searchMode
|
|
* A search mode object.
|
|
* @param {string} searchMode.engineName
|
|
* The name of the search engine to restrict to.
|
|
* @param {UrlbarUtils.RESULT_SOURCE} searchMode.source
|
|
* A result source to restrict to.
|
|
* @param {string} searchMode.entry
|
|
* How search mode was entered. This is recorded in event telemetry. One of
|
|
* the values in UrlbarUtils.SEARCH_MODE_ENTRY.
|
|
* @param {boolean} [searchMode.isPreview]
|
|
* If true, we will preview search mode. Search mode preview does not record
|
|
* telemetry and has slighly different UI behavior. The preview is exited in
|
|
* favor of full search mode when a query is executed. False should be
|
|
* passed if the caller needs to enter search mode but expects it will not
|
|
* be interacted with right away. Defaults to true.
|
|
* @param {Browser} browser
|
|
* The browser for which to set search mode.
|
|
*/
|
|
async setSearchMode(searchMode, browser) {
|
|
let currentSearchMode = this.getSearchMode(browser);
|
|
let areSearchModesSame =
|
|
(!currentSearchMode && !searchMode) ||
|
|
lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode);
|
|
|
|
// Exit search mode if the passed-in engine is invalid or hidden.
|
|
let engine;
|
|
if (searchMode?.engineName) {
|
|
if (!Services.search.isInitialized) {
|
|
await Services.search.init();
|
|
}
|
|
engine = Services.search.getEngineByName(searchMode.engineName);
|
|
if (!engine || engine.hidden) {
|
|
searchMode = null;
|
|
}
|
|
}
|
|
|
|
let { engineName, source, entry, isPreview = true } = searchMode || {};
|
|
|
|
searchMode = null;
|
|
|
|
if (engineName) {
|
|
searchMode = {
|
|
engineName,
|
|
isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
|
|
};
|
|
if (source) {
|
|
searchMode.source = source;
|
|
} else if (searchMode.isGeneralPurposeEngine) {
|
|
// History results for general-purpose search engines are often not
|
|
// useful, so we hide them in search mode. See bug 1658646 for
|
|
// discussion.
|
|
searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
|
|
}
|
|
} else if (source) {
|
|
let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
|
|
if (sourceName) {
|
|
searchMode = { source };
|
|
} else {
|
|
console.error(`Unrecognized source: ${source}`);
|
|
}
|
|
}
|
|
|
|
if (searchMode) {
|
|
searchMode.isPreview = isPreview;
|
|
if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) {
|
|
searchMode.entry = entry;
|
|
} else {
|
|
// If we see this value showing up in telemetry, we should review
|
|
// search mode's entry points.
|
|
searchMode.entry = "other";
|
|
}
|
|
|
|
// Add the search mode to the map.
|
|
if (!searchMode.isPreview) {
|
|
this._searchModesByBrowser.set(browser, {
|
|
confirmed: searchMode,
|
|
});
|
|
} else {
|
|
let modes = this._searchModesByBrowser.get(browser) || {};
|
|
modes.preview = searchMode;
|
|
this._searchModesByBrowser.set(browser, modes);
|
|
}
|
|
} else {
|
|
this._searchModesByBrowser.delete(browser);
|
|
}
|
|
|
|
// Enter search mode if the browser is selected.
|
|
if (browser == this.window.gBrowser.selectedBrowser) {
|
|
this._updateSearchModeUI(searchMode);
|
|
if (searchMode) {
|
|
// Set userTypedValue to the query string so that it's properly restored
|
|
// when switching back to the current tab and across sessions.
|
|
this.window.gBrowser.userTypedValue = this.untrimmedValue;
|
|
this.valueIsTyped = true;
|
|
if (!searchMode.isPreview && !areSearchModesSame) {
|
|
try {
|
|
lazy.BrowserSearchTelemetry.recordSearchMode(searchMode);
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restores the current browser search mode from a previously stored state.
|
|
*/
|
|
restoreSearchModeState() {
|
|
let modes = this._searchModesByBrowser.get(
|
|
this.window.gBrowser.selectedBrowser
|
|
);
|
|
this.searchMode = modes?.confirmed;
|
|
}
|
|
|
|
/**
|
|
* Enters search mode with the default engine.
|
|
*/
|
|
searchModeShortcut() {
|
|
// We restrict to search results when entering search mode from this
|
|
// shortcut to honor historical behaviour.
|
|
this.searchMode = {
|
|
source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
|
|
engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate).name,
|
|
entry: "shortcut",
|
|
};
|
|
// The searchMode setter clears the input if pageproxystate is valid, so
|
|
// we know at this point this.value will either be blank or the user's
|
|
// typed string.
|
|
this.search(this.value);
|
|
this.select();
|
|
}
|
|
|
|
/**
|
|
* Confirms the current search mode.
|
|
*/
|
|
confirmSearchMode() {
|
|
let searchMode = this.searchMode;
|
|
if (searchMode?.isPreview) {
|
|
searchMode.isPreview = false;
|
|
this.searchMode = searchMode;
|
|
|
|
// Unselect the one-off search button to ensure UI consistency.
|
|
this.view.oneOffSearchButtons.selectedButton = null;
|
|
}
|
|
}
|
|
|
|
// Getters and Setters below.
|
|
|
|
get editor() {
|
|
return this.inputField.editor;
|
|
}
|
|
|
|
get focused() {
|
|
return this.document.activeElement == this.inputField;
|
|
}
|
|
|
|
get goButton() {
|
|
return this.querySelector("#urlbar-go-button");
|
|
}
|
|
|
|
get value() {
|
|
return this.inputField.value;
|
|
}
|
|
|
|
get untrimmedValue() {
|
|
return this._untrimmedValue;
|
|
}
|
|
|
|
set value(val) {
|
|
this._setValue(val, true);
|
|
}
|
|
|
|
get lastSearchString() {
|
|
return this._lastSearchString;
|
|
}
|
|
|
|
get searchMode() {
|
|
return this.getSearchMode(this.window.gBrowser.selectedBrowser);
|
|
}
|
|
|
|
set searchMode(searchMode) {
|
|
this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser);
|
|
}
|
|
|
|
async updateLayoutBreakout() {
|
|
if (!this._toolbar) {
|
|
// Expanding requires a parent toolbar.
|
|
return;
|
|
}
|
|
if (this.document.fullscreenElement) {
|
|
// Toolbars are hidden in DOM fullscreen mode, so we can't get proper
|
|
// layout information and need to retry after leaving that mode.
|
|
this.window.addEventListener(
|
|
"fullscreen",
|
|
() => {
|
|
this.updateLayoutBreakout();
|
|
},
|
|
{ once: true }
|
|
);
|
|
return;
|
|
}
|
|
await this._updateLayoutBreakoutDimensions();
|
|
}
|
|
|
|
startLayoutExtend() {
|
|
// Do not expand if:
|
|
// The Urlbar does not support being expanded or it is already expanded
|
|
if (
|
|
!this.hasAttribute("breakout") ||
|
|
this.hasAttribute("breakout-extend")
|
|
) {
|
|
return;
|
|
}
|
|
if (!this.view.isOpen) {
|
|
return;
|
|
}
|
|
|
|
if (Cu.isInAutomation) {
|
|
if (lazy.UrlbarPrefs.get("disableExtendForTests")) {
|
|
this.setAttribute("breakout-extend-disabled", "true");
|
|
return;
|
|
}
|
|
this.removeAttribute("breakout-extend-disabled");
|
|
}
|
|
|
|
this._toolbar.setAttribute("urlbar-exceeds-toolbar-bounds", "true");
|
|
this.setAttribute("breakout-extend", "true");
|
|
|
|
// Enable the animation only after the first extend call to ensure it
|
|
// doesn't run when opening a new window.
|
|
if (!this.hasAttribute("breakout-extend-animate")) {
|
|
this.window.promiseDocumentFlushed(() => {
|
|
this.window.requestAnimationFrame(() => {
|
|
this.setAttribute("breakout-extend-animate", "true");
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
endLayoutExtend() {
|
|
// If reduce motion is enabled, we want to collapse the Urlbar here so the
|
|
// user sees only sees two states: not expanded, and expanded with the view
|
|
// open.
|
|
if (!this.hasAttribute("breakout-extend") || this.view.isOpen) {
|
|
return;
|
|
}
|
|
|
|
this.removeAttribute("breakout-extend");
|
|
this._toolbar.removeAttribute("urlbar-exceeds-toolbar-bounds");
|
|
}
|
|
|
|
/**
|
|
* Updates the user interface to indicate whether the URI in the address bar
|
|
* is different than the loaded page, because it's being edited or because a
|
|
* search result is currently selected and is displayed in the location bar.
|
|
*
|
|
* @param {string} state
|
|
* The string "valid" indicates that the security indicators and other
|
|
* related user interface elments should be shown because the URI in
|
|
* the location bar matches the loaded page. The string "invalid"
|
|
* indicates that the URI in the location bar is different than the
|
|
* loaded page.
|
|
* @param {boolean} [updatePopupNotifications]
|
|
* Indicates whether we should update the PopupNotifications
|
|
* visibility due to this change, otherwise avoid doing so as it is
|
|
* being handled somewhere else.
|
|
*/
|
|
setPageProxyState(state, updatePopupNotifications) {
|
|
let prevState = this.getAttribute("pageproxystate");
|
|
|
|
this.setAttribute("pageproxystate", state);
|
|
this._inputContainer.setAttribute("pageproxystate", state);
|
|
this._identityBox.setAttribute("pageproxystate", state);
|
|
|
|
if (state == "valid") {
|
|
this._lastValidURLStr = this.value;
|
|
}
|
|
|
|
if (
|
|
updatePopupNotifications &&
|
|
prevState != state &&
|
|
this.window.UpdatePopupNotificationsVisibility
|
|
) {
|
|
this.window.UpdatePopupNotificationsVisibility();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When switching tabs quickly, TabSelect sometimes happens before
|
|
* _adjustFocusAfterTabSwitch and due to the focus still being on the old
|
|
* tab, we end up flickering the results pane briefly.
|
|
*/
|
|
afterTabSwitchFocusChange() {
|
|
this._gotFocusChange = true;
|
|
this._afterTabSelectAndFocusChange();
|
|
}
|
|
|
|
/**
|
|
* Confirms search mode and starts a new search if appropriate for the given
|
|
* result. See also _searchModeForResult.
|
|
*
|
|
* @param {object} options
|
|
* Options object.
|
|
* @param {string} options.entry
|
|
* The search mode entry point. See setSearchMode documentation for details.
|
|
* @param {UrlbarResult} [options.result]
|
|
* The result to confirm. Defaults to the currently selected result.
|
|
* @param {boolean} [options.checkValue]
|
|
* If true, the trimmed input value must equal the result's keyword in order
|
|
* to enter search mode.
|
|
* @param {boolean} [options.startQuery]
|
|
* If true, start a query after entering search mode. Defaults to true.
|
|
* @returns {boolean}
|
|
* True if we entered search mode and false if not.
|
|
*/
|
|
maybeConfirmSearchModeFromResult({
|
|
entry,
|
|
result = this._resultForCurrentValue,
|
|
checkValue = true,
|
|
startQuery = true,
|
|
}) {
|
|
if (
|
|
!result ||
|
|
(checkValue && this.value.trim() != result.payload.keyword?.trim())
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
let searchMode = this._searchModeForResult(result, entry);
|
|
if (!searchMode) {
|
|
return false;
|
|
}
|
|
|
|
this.searchMode = searchMode;
|
|
|
|
let value = result.payload.query?.trimStart() || "";
|
|
this._setValue(value, false);
|
|
|
|
if (startQuery) {
|
|
this.startQuery({ allowAutofill: false });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: {
|
|
switch (data) {
|
|
case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
|
|
case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: {
|
|
let searchMode = this.searchMode;
|
|
let engine = subject.QueryInterface(Ci.nsISearchEngine);
|
|
if (searchMode?.engineName == engine.name) {
|
|
// Exit search mode if the current search mode engine was removed.
|
|
this.searchMode = searchMode;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get search source.
|
|
*
|
|
* @param {Event} event
|
|
* The event that triggered this query.
|
|
* @returns {string}
|
|
* The source name.
|
|
*/
|
|
getSearchSource(event) {
|
|
if (this._isHandoffSession) {
|
|
return "urlbar-handoff";
|
|
}
|
|
|
|
const isOneOff = this.view.oneOffSearchButtons.eventTargetIsAOneOff(event);
|
|
if (this.searchMode && !isOneOff) {
|
|
// Without checking !isOneOff, we might record the string
|
|
// oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to
|
|
// oneoff_urlbar and oneoff_searchbar). The extra information is not
|
|
// necessary; the intent is the same regardless of whether the user is
|
|
// in search mode when they do a key-modified click/enter on a one-off.
|
|
return "urlbar-searchmode";
|
|
}
|
|
|
|
if (this.window.gBrowser.selectedBrowser.searchTerms && !isOneOff) {
|
|
return "urlbar-persisted";
|
|
}
|
|
|
|
return "urlbar";
|
|
}
|
|
|
|
// Private methods below.
|
|
|
|
_addObservers() {
|
|
Services.obs.addObserver(
|
|
this,
|
|
lazy.SearchUtils.TOPIC_ENGINE_MODIFIED,
|
|
true
|
|
);
|
|
}
|
|
|
|
_getURIFixupInfo(searchString) {
|
|
let flags =
|
|
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
|
|
Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
|
|
if (this.isPrivate) {
|
|
flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
|
|
}
|
|
try {
|
|
return Services.uriFixup.getFixupURIInfo(searchString, flags);
|
|
} catch (ex) {
|
|
console.error(
|
|
`An error occured while trying to fixup "${searchString}": ${ex}`
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_afterTabSelectAndFocusChange() {
|
|
// We must have seen both events to proceed safely.
|
|
if (!this._gotFocusChange || !this._gotTabSelect) {
|
|
return;
|
|
}
|
|
this._gotFocusChange = this._gotTabSelect = false;
|
|
|
|
this._resetSearchState();
|
|
|
|
// Switching tabs doesn't always change urlbar focus, so we must try to
|
|
// reopen here too, not just on focus.
|
|
// We don't use the original TabSelect event because caching it causes
|
|
// leaks on MacOS.
|
|
if (this.view.autoOpen({ event: new CustomEvent("tabswitch") })) {
|
|
return;
|
|
}
|
|
// The input may retain focus when switching tabs in which case we
|
|
// need to close the view explicitly.
|
|
this.view.close();
|
|
}
|
|
|
|
async _updateLayoutBreakoutDimensions() {
|
|
// When this method gets called a second time before the first call
|
|
// finishes, we need to disregard the first one.
|
|
let updateKey = {};
|
|
this._layoutBreakoutUpdateKey = updateKey;
|
|
|
|
this.removeAttribute("breakout");
|
|
this.textbox.parentNode.removeAttribute("breakout");
|
|
|
|
await this.window.promiseDocumentFlushed(() => {});
|
|
await new Promise(resolve => {
|
|
this.window.requestAnimationFrame(() => {
|
|
if (this._layoutBreakoutUpdateKey != updateKey) {
|
|
return;
|
|
}
|
|
|
|
this.textbox.parentNode.style.setProperty(
|
|
"--urlbar-container-height",
|
|
px(getBoundsWithoutFlushing(this.textbox.parentNode).height)
|
|
);
|
|
this.textbox.style.setProperty(
|
|
"--urlbar-height",
|
|
px(getBoundsWithoutFlushing(this.textbox).height)
|
|
);
|
|
this.textbox.style.setProperty(
|
|
"--urlbar-toolbar-height",
|
|
px(getBoundsWithoutFlushing(this._toolbar).height)
|
|
);
|
|
|
|
this.setAttribute("breakout", "true");
|
|
this.textbox.parentNode.setAttribute("breakout", "true");
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
_setValue(val, allowTrim) {
|
|
// Don't expose internal about:reader URLs to the user.
|
|
let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
|
|
if (originalUrl) {
|
|
val = originalUrl.displaySpec;
|
|
}
|
|
this._untrimmedValue = val;
|
|
|
|
if (allowTrim) {
|
|
val = this._trimValue(val);
|
|
}
|
|
|
|
this.valueIsTyped = false;
|
|
this._resultForCurrentValue = null;
|
|
this.inputField.value = val;
|
|
this.formatValue();
|
|
this.removeAttribute("actiontype");
|
|
|
|
// Dispatch ValueChange event for accessibility.
|
|
let event = this.document.createEvent("Events");
|
|
event.initEvent("ValueChange", true, true);
|
|
this.inputField.dispatchEvent(event);
|
|
|
|
return val;
|
|
}
|
|
|
|
_getValueFromResult(result, urlOverride = null) {
|
|
switch (result.type) {
|
|
case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
|
|
return result.payload.input;
|
|
case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
|
|
let value = "";
|
|
if (result.payload.keyword) {
|
|
value += result.payload.keyword + " ";
|
|
}
|
|
value += result.payload.suggestion || result.payload.query;
|
|
return value;
|
|
}
|
|
case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
|
|
return result.payload.content;
|
|
case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC:
|
|
return result.payload.input || "";
|
|
}
|
|
|
|
if (urlOverride === "") {
|
|
// Allow callers to clear the input.
|
|
return "";
|
|
}
|
|
|
|
try {
|
|
let uri = Services.io.newURI(urlOverride || result.payload.url);
|
|
if (uri) {
|
|
return losslessDecodeURI(uri);
|
|
}
|
|
} catch (ex) {}
|
|
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Resets some state so that searches from the user's previous interaction
|
|
* with the input don't interfere with searches from a new interaction.
|
|
*/
|
|
_resetSearchState() {
|
|
this._lastSearchString = this.value;
|
|
this._autofillPlaceholder = null;
|
|
}
|
|
|
|
/**
|
|
* Autofills the autofill placeholder string if appropriate, and determines
|
|
* whether autofill should be allowed for the new search started by an input
|
|
* event.
|
|
*
|
|
* @param {string} value
|
|
* The new search string.
|
|
* @returns {boolean}
|
|
* Whether autofill should be allowed in the new search.
|
|
*/
|
|
_maybeAutofillPlaceholder(value) {
|
|
// We allow autofill in local but not remote search modes.
|
|
let allowAutofill =
|
|
this.selectionEnd == value.length &&
|
|
!this.searchMode?.engineName &&
|
|
this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
|
|
if (!allowAutofill) {
|
|
this._autofillPlaceholder = null;
|
|
return false;
|
|
}
|
|
|
|
// Determine whether we can autofill the placeholder. The placeholder is a
|
|
// value that we autofill now, when the search starts and before we wait on
|
|
// its first result, in order to prevent a flicker in the input caused by
|
|
// the previous autofilled substring disappearing and reappearing when the
|
|
// first result arrives. Of course we can only autofill the placeholder if
|
|
// it starts with the new search string, and we shouldn't autofill anything
|
|
// if the caret isn't at the end of the input.
|
|
let canAutofillPlaceholder = false;
|
|
if (this._autofillPlaceholder) {
|
|
if (this._autofillPlaceholder.type == "adaptive") {
|
|
canAutofillPlaceholder =
|
|
value.length >=
|
|
this._autofillPlaceholder.adaptiveHistoryInput.length &&
|
|
this._autofillPlaceholder.value
|
|
.toLocaleLowerCase()
|
|
.startsWith(value.toLocaleLowerCase());
|
|
} else {
|
|
canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL(
|
|
this._autofillPlaceholder.value,
|
|
value
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!canAutofillPlaceholder) {
|
|
this._autofillPlaceholder = null;
|
|
} else if (
|
|
this._autofillPlaceholder &&
|
|
this.selectionEnd == this.value.length &&
|
|
this._enableAutofillPlaceholder
|
|
) {
|
|
let autofillValue =
|
|
value + this._autofillPlaceholder.value.substring(value.length);
|
|
this._autofillValue({
|
|
value: autofillValue,
|
|
selectionStart: value.length,
|
|
selectionEnd: autofillValue.length,
|
|
type: this._autofillPlaceholder.type,
|
|
adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput,
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_checkForRtlText(value) {
|
|
let directionality = this.window.windowUtils.getDirectionFromText(value);
|
|
if (directionality == this.window.windowUtils.DIRECTION_RTL) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Invoked on overflow/underflow/scrollend events to update attributes
|
|
* related to the input text directionality. Overflow fade masks use these
|
|
* attributes to appear at the proper side of the urlbar.
|
|
*/
|
|
updateTextOverflow() {
|
|
if (!this._overflowing) {
|
|
this.removeAttribute("textoverflow");
|
|
return;
|
|
}
|
|
|
|
let isRTL =
|
|
this.getAttribute("domaindir") === "rtl" &&
|
|
this._checkForRtlText(this.value);
|
|
|
|
this.window.promiseDocumentFlushed(() => {
|
|
// Check overflow again to ensure it didn't change in the meanwhile.
|
|
let input = this.inputField;
|
|
if (input && this._overflowing) {
|
|
// Normally we would overflow at the final side of text direction,
|
|
// though RTL domains may cause us to overflow at the opposite side.
|
|
// This happens dynamically as a consequence of the input field contents
|
|
// and the call to _ensureFormattedHostVisible, this code only reports
|
|
// the final state of all that scrolling into an attribute, because
|
|
// there's no other way to capture this in css.
|
|
// Note it's also possible to scroll an unfocused input field using
|
|
// SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad
|
|
// scroll (without modifiers) on Mac.
|
|
let side = "both";
|
|
if (isRTL) {
|
|
if (input.scrollLeft == 0) {
|
|
side = "left";
|
|
} else if (input.scrollLeft == input.scrollLeftMin) {
|
|
side = "right";
|
|
}
|
|
} else if (input.scrollLeft == 0) {
|
|
side = "right";
|
|
} else if (input.scrollLeft == input.scrollLeftMax) {
|
|
side = "left";
|
|
}
|
|
|
|
this.window.requestAnimationFrame(() => {
|
|
// And check once again, since we might have stopped overflowing
|
|
// since the promiseDocumentFlushed callback fired.
|
|
if (this._overflowing) {
|
|
this.setAttribute("textoverflow", side);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
_updateUrlTooltip() {
|
|
if (this.focused || !this._overflowing) {
|
|
this.inputField.removeAttribute("title");
|
|
} else {
|
|
this.inputField.setAttribute("title", this.untrimmedValue);
|
|
}
|
|
}
|
|
|
|
_getSelectedValueForClipboard() {
|
|
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 = this.value.replace(selectedVal, "");
|
|
if (remainder != "" && remainder[0] != "/") {
|
|
return selectedVal;
|
|
}
|
|
}
|
|
|
|
let uri;
|
|
if (this.getAttribute("pageproxystate") == "valid") {
|
|
uri = this.window.gBrowser.currentURI;
|
|
} else {
|
|
// The value could be:
|
|
// 1. a trimmed url, set by selecting a result
|
|
// 2. a search string set by selecting a result
|
|
// 3. a url that was confirmed but didn't finish loading yet
|
|
// If it's an url the untrimmedValue should resolve to a valid URI,
|
|
// otherwise it's a search string that should be copied as-is.
|
|
try {
|
|
uri = Services.io.newURI(this._untrimmedValue);
|
|
} catch (ex) {
|
|
return selectedVal;
|
|
}
|
|
}
|
|
uri = this.makeURIReadable(uri);
|
|
let displaySpec = uri.displaySpec;
|
|
|
|
// 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 (
|
|
this.value == selectedVal &&
|
|
!uri.schemeIs("javascript") &&
|
|
!uri.schemeIs("data") &&
|
|
!lazy.UrlbarPrefs.get("decodeURLsOnCopy")
|
|
) {
|
|
return displaySpec;
|
|
}
|
|
|
|
// Just the beginning of the URL is selected, or we want a decoded
|
|
// url. First check for a trimmed value.
|
|
|
|
if (
|
|
!selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
|
|
// Note _trimValue may also trim a trailing slash, thus we can't just do
|
|
// a straight string compare to tell if the protocol was trimmed.
|
|
!displaySpec.startsWith(this._trimValue(displaySpec))
|
|
) {
|
|
selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal;
|
|
}
|
|
|
|
// If selection starts from the beginning and part or all of the URL
|
|
// is selected, we check for decoded characters and encode them.
|
|
// Unless decodeURLsOnCopy is set. Do not encode data: URIs.
|
|
if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) {
|
|
try {
|
|
new URL(selectedVal);
|
|
// Use encodeURI instead of URL.href because we don't want
|
|
// trailing slash.
|
|
selectedVal = encodeURI(selectedVal);
|
|
} catch (ex) {
|
|
// URL is invalid. Return original selected value.
|
|
}
|
|
}
|
|
|
|
return selectedVal;
|
|
}
|
|
|
|
_toggleActionOverride(event) {
|
|
// Ignore repeated KeyboardEvents.
|
|
if (event.repeat) {
|
|
return;
|
|
}
|
|
if (
|
|
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
|
|
event.keyCode == KeyEvent.DOM_VK_ALT ||
|
|
event.keyCode ==
|
|
(AppConstants.platform == "macosx"
|
|
? KeyEvent.DOM_VK_META
|
|
: KeyEvent.DOM_VK_CONTROL)
|
|
) {
|
|
if (event.type == "keydown") {
|
|
this._actionOverrideKeyCount++;
|
|
this.setAttribute("actionoverride", "true");
|
|
this.view.panel.setAttribute("actionoverride", "true");
|
|
} else if (
|
|
this._actionOverrideKeyCount &&
|
|
--this._actionOverrideKeyCount == 0
|
|
) {
|
|
this._clearActionOverride();
|
|
}
|
|
}
|
|
}
|
|
|
|
_clearActionOverride() {
|
|
this._actionOverrideKeyCount = 0;
|
|
this.removeAttribute("actionoverride");
|
|
this.view.panel.removeAttribute("actionoverride");
|
|
}
|
|
|
|
/**
|
|
* Get the url to load for the search query and records in telemetry that it
|
|
* is being loaded.
|
|
*
|
|
* @param {nsISearchEngine} engine
|
|
* The engine to generate the query for.
|
|
* @param {Event} event
|
|
* The event that triggered this query.
|
|
* @param {object} searchActionDetails
|
|
* The details associated with this search query.
|
|
* @param {boolean} searchActionDetails.isSuggestion
|
|
* True if this query was initiated from a suggestion from the search engine.
|
|
* @param {boolean} searchActionDetails.alias
|
|
* True if this query was initiated via a search alias.
|
|
* @param {boolean} searchActionDetails.isFormHistory
|
|
* True if this query was initiated from a form history result.
|
|
* @param {string} searchActionDetails.url
|
|
* The url this query was triggered with.
|
|
*/
|
|
_recordSearch(engine, event, searchActionDetails = {}) {
|
|
const isOneOff = this.view.oneOffSearchButtons.eventTargetIsAOneOff(event);
|
|
|
|
lazy.BrowserSearchTelemetry.recordSearch(
|
|
this.window.gBrowser.selectedBrowser,
|
|
engine,
|
|
this.getSearchSource(event),
|
|
{
|
|
...searchActionDetails,
|
|
isOneOff,
|
|
newtabSessionId: this._handoffSession,
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Shortens the given value, usually by removing http:// and trailing slashes.
|
|
*
|
|
* @param {string} val
|
|
* The string to be trimmed if it appears to be URI
|
|
* @returns {string}
|
|
* The trimmed string
|
|
*/
|
|
_trimValue(val) {
|
|
return lazy.UrlbarPrefs.get("trimURLs")
|
|
? lazy.BrowserUIUtils.trimURL(val)
|
|
: val;
|
|
}
|
|
|
|
/**
|
|
* If appropriate, this prefixes a search string with 'www.' and suffixes it
|
|
* with browser.fixup.alternate.suffix prior to navigating.
|
|
*
|
|
* @param {Event} event
|
|
* The event that triggered this query.
|
|
* @param {string} value
|
|
* The search string that should be canonized.
|
|
* @returns {string}
|
|
* Returns the canonized URL if available and null otherwise.
|
|
*/
|
|
_maybeCanonizeURL(event, value) {
|
|
// 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 (
|
|
!KeyboardEvent.isInstance(event) ||
|
|
event._disableCanonization ||
|
|
!event.ctrlKey ||
|
|
!lazy.UrlbarPrefs.get("ctrlCanonizesURLs") ||
|
|
!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
|
|
if (!suffix.endsWith("/")) {
|
|
suffix += "/";
|
|
}
|
|
|
|
// trim leading/trailing spaces (bug 233205)
|
|
value = value.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 = value.indexOf("/");
|
|
if (firstSlash >= 0) {
|
|
value =
|
|
value.substring(0, firstSlash) +
|
|
suffix +
|
|
value.substring(firstSlash + 1);
|
|
} else {
|
|
value = value + suffix;
|
|
}
|
|
|
|
try {
|
|
const info = Services.uriFixup.getFixupURIInfo(
|
|
value,
|
|
Ci.nsIURIFixup.FIXUP_FLAG_FORCE_ALTERNATE_URI
|
|
);
|
|
value = info.fixedURI.spec;
|
|
} catch (ex) {
|
|
console.error(`An error occured while trying to fixup "${value}": ${ex}`);
|
|
}
|
|
|
|
this.value = value;
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Autofills a value into the input. The value will be autofilled regardless
|
|
* of the input's current value.
|
|
*
|
|
* @param {object} options
|
|
* The options object.
|
|
* @param {string} options.value
|
|
* The value to autofill.
|
|
* @param {integer} options.selectionStart
|
|
* The new selectionStart.
|
|
* @param {integer} options.selectionEnd
|
|
* The new selectionEnd.
|
|
* @param {"origin" | "url" | "adaptive"} options.type
|
|
* The autofill type, one of: "origin", "url", "adaptive"
|
|
* @param {string} options.adaptiveHistoryInput
|
|
* If the autofill type is "adaptive", this is the matching `input` value
|
|
* from adaptive history.
|
|
*/
|
|
_autofillValue({
|
|
value,
|
|
selectionStart,
|
|
selectionEnd,
|
|
type,
|
|
adaptiveHistoryInput,
|
|
}) {
|
|
// The autofilled value may be a URL that includes a scheme at the
|
|
// beginning. Do not allow it to be trimmed.
|
|
this._setValue(value, false);
|
|
this.inputField.setSelectionRange(selectionStart, selectionEnd);
|
|
this._autofillPlaceholder = { value, type, adaptiveHistoryInput };
|
|
}
|
|
|
|
/**
|
|
* Loads the url in the appropriate place.
|
|
*
|
|
* @param {string} url
|
|
* The URL to open.
|
|
* @param {Event} event
|
|
* The event that triggered to load the url.
|
|
* @param {string} openUILinkWhere
|
|
* Where we expect the result to be opened.
|
|
* @param {object} params
|
|
* The parameters related to how and where the result will be opened.
|
|
* Further supported paramters are listed in utilityOverlay.js#openUILinkIn.
|
|
* @param {object} params.triggeringPrincipal
|
|
* The principal that the action was triggered from.
|
|
* @param {nsIInputStream} [params.postData]
|
|
* The POST data associated with a search submission.
|
|
* @param {boolean} [params.allowInheritPrincipal]
|
|
* Whether the principal can be inherited.
|
|
* @param {object} [resultDetails]
|
|
* Details of the selected result, if any.
|
|
* @param {UrlbarUtils.RESULT_TYPE} [resultDetails.type]
|
|
* Details of the result type, if any.
|
|
* @param {string} [resultDetails.searchTerm]
|
|
* Search term of the result source, if any.
|
|
* @param {UrlbarUtils.RESULT_SOURCE} [resultDetails.source]
|
|
* Details of the result source, if any.
|
|
* @param {object} browser [optional] the browser to use for the load.
|
|
*/
|
|
_loadURL(
|
|
url,
|
|
event,
|
|
openUILinkWhere,
|
|
params,
|
|
resultDetails = null,
|
|
browser = this.window.gBrowser.selectedBrowser
|
|
) {
|
|
// No point in setting these because we'll handleRevert() a few rows below.
|
|
if (openUILinkWhere == "current") {
|
|
this.value =
|
|
lazy.UrlbarPrefs.get("showSearchTermsFeatureGate") &&
|
|
lazy.UrlbarPrefs.get("showSearchTerms.enabled") &&
|
|
resultDetails?.searchTerm
|
|
? resultDetails.searchTerm
|
|
: url;
|
|
browser.userTypedValue = this.value;
|
|
}
|
|
|
|
// No point in setting this if we are loading in a new window.
|
|
if (
|
|
openUILinkWhere != "window" &&
|
|
this.window.gInitialPages.includes(url)
|
|
) {
|
|
browser.initialPageLoadedFromUserAction = url;
|
|
}
|
|
|
|
try {
|
|
lazy.UrlbarUtils.addToUrlbarHistory(url, this.window);
|
|
} catch (ex) {
|
|
// Things may go wrong when adding url to session history,
|
|
// but don't let that interfere with the loading of the url.
|
|
console.error(ex);
|
|
}
|
|
|
|
// TODO: When bug 1498553 is resolved, we should be able to
|
|
// remove the !triggeringPrincipal condition here.
|
|
if (
|
|
!params.triggeringPrincipal ||
|
|
params.triggeringPrincipal.isSystemPrincipal
|
|
) {
|
|
// Reset DOS mitigations for the basic auth prompt.
|
|
delete browser.authPromptAbuseCounter;
|
|
|
|
// Reset temporary permissions on the current tab if the user reloads
|
|
// the tab via the urlbar.
|
|
if (
|
|
openUILinkWhere == "current" &&
|
|
browser.currentURI &&
|
|
url === browser.currentURI.spec
|
|
) {
|
|
this.window.SitePermissions.clearTemporaryBlockPermissions(browser);
|
|
}
|
|
}
|
|
|
|
params.allowThirdPartyFixup = true;
|
|
|
|
if (openUILinkWhere == "current") {
|
|
params.targetBrowser = browser;
|
|
params.indicateErrorPageLoad = true;
|
|
params.allowPinnedTabHostChange = true;
|
|
params.allowPopups = url.startsWith("javascript:");
|
|
} else {
|
|
params.initiatingDoc = this.window.document;
|
|
}
|
|
|
|
if (
|
|
this._keyDownEnterDeferred &&
|
|
event?.keyCode === KeyEvent.DOM_VK_RETURN &&
|
|
openUILinkWhere === "current"
|
|
) {
|
|
// In this case, we move the focus to the browser that loads the content
|
|
// upon key up the enter key.
|
|
// To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid
|
|
// focusing on the browser in the function. And also, set loadedContent
|
|
// flag that whether the content is loaded in the current tab by this enter
|
|
// key. _keyDownEnterDeferred promise is processed at key up the enter,
|
|
// focus on the browser passed by _keyDownEnterDeferred.resolve().
|
|
params.avoidBrowserFocus = true;
|
|
this._keyDownEnterDeferred.loadedContent = true;
|
|
this._keyDownEnterDeferred.resolve(browser);
|
|
}
|
|
|
|
// Ensure the window gets the `private` feature if the current window
|
|
// is private, unless the caller explicitly requested not to.
|
|
if (this.isPrivate && !("private" in params)) {
|
|
params.private = true;
|
|
}
|
|
|
|
// 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.
|
|
if (!params.avoidBrowserFocus) {
|
|
browser.focus();
|
|
// Make sure the domain name stays visible for spoof protection and usability.
|
|
this.inputField.setSelectionRange(0, 0);
|
|
}
|
|
|
|
if (openUILinkWhere != "current") {
|
|
this.handleRevert();
|
|
}
|
|
|
|
// Notify about the start of navigation.
|
|
this._notifyStartNavigation(resultDetails);
|
|
|
|
try {
|
|
this.window.openTrustedLinkIn(url, openUILinkWhere, params);
|
|
} catch (ex) {
|
|
// This load can throw an exception in certain cases, which means
|
|
// we'll want to replace the URL with the loaded URL:
|
|
if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
|
|
this.handleRevert();
|
|
}
|
|
}
|
|
|
|
// If we show the focus border after closing the view, it would appear to
|
|
// flash since this._on_blur would remove it immediately after.
|
|
this.view.close({ showFocusBorder: false });
|
|
}
|
|
|
|
/**
|
|
* Determines where a URL/page should be opened.
|
|
*
|
|
* @param {Event} event the event triggering the opening.
|
|
* @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
|
|
*/
|
|
_whereToOpen(event) {
|
|
let isKeyboardEvent = KeyboardEvent.isInstance(event);
|
|
let reuseEmpty = isKeyboardEvent;
|
|
let where = undefined;
|
|
if (
|
|
isKeyboardEvent &&
|
|
(event.altKey || event.getModifierState("AltGraph"))
|
|
) {
|
|
// We support using 'alt' to open in a tab, because ctrl/shift
|
|
// might be used for canonizing URLs:
|
|
where = event.shiftKey ? "tabshifted" : "tab";
|
|
} else if (
|
|
isKeyboardEvent &&
|
|
event.ctrlKey &&
|
|
lazy.UrlbarPrefs.get("ctrlCanonizesURLs")
|
|
) {
|
|
// If we're allowing canonization, and this is a key event with ctrl
|
|
// pressed, open in current tab to allow ctrl-enter to canonize URL.
|
|
where = "current";
|
|
} else {
|
|
where = this.window.whereToOpenLink(event, false, false);
|
|
}
|
|
if (lazy.UrlbarPrefs.get("openintab")) {
|
|
if (where == "current") {
|
|
where = "tab";
|
|
} else if (where == "tab") {
|
|
where = "current";
|
|
}
|
|
reuseEmpty = true;
|
|
}
|
|
if (
|
|
where == "tab" &&
|
|
reuseEmpty &&
|
|
this.window.gBrowser.selectedTab.isEmpty
|
|
) {
|
|
where = "current";
|
|
}
|
|
return where;
|
|
}
|
|
|
|
_initCopyCutController() {
|
|
this._copyCutController = new CopyCutController(this);
|
|
this.inputField.controllers.insertControllerAt(0, this._copyCutController);
|
|
}
|
|
|
|
_initPasteAndGo() {
|
|
let inputBox = this.querySelector("moz-input-box");
|
|
let contextMenu = inputBox.menupopup;
|
|
let insertLocation = contextMenu.firstElementChild;
|
|
while (
|
|
insertLocation.nextElementSibling &&
|
|
insertLocation.getAttribute("cmd") != "cmd_paste"
|
|
) {
|
|
insertLocation = insertLocation.nextElementSibling;
|
|
}
|
|
if (!insertLocation) {
|
|
return;
|
|
}
|
|
|
|
let pasteAndGo = this.document.createXULElement("menuitem");
|
|
pasteAndGo.id = "paste-and-go";
|
|
let label = Services.strings
|
|
.createBundle("chrome://browser/locale/browser.properties")
|
|
.GetStringFromName("pasteAndGo.label");
|
|
pasteAndGo.setAttribute("label", label);
|
|
pasteAndGo.setAttribute("anonid", "paste-and-go");
|
|
pasteAndGo.addEventListener("command", () => {
|
|
this._suppressStartQuery = true;
|
|
|
|
this.select();
|
|
this.window.goDoCommand("cmd_paste");
|
|
this.setResultForCurrentValue(null);
|
|
this.controller.clearLastQueryContextCache();
|
|
this.handleCommand();
|
|
|
|
this._suppressStartQuery = false;
|
|
});
|
|
|
|
contextMenu.addEventListener("popupshowing", () => {
|
|
// Close the results pane when the input field contextual menu is open,
|
|
// because paste and go doesn't want a result selection.
|
|
this.view.close();
|
|
|
|
let controller = this.document.commandDispatcher.getControllerForCommand(
|
|
"cmd_paste"
|
|
);
|
|
let enabled = controller.isCommandEnabled("cmd_paste");
|
|
if (enabled) {
|
|
pasteAndGo.removeAttribute("disabled");
|
|
} else {
|
|
pasteAndGo.setAttribute("disabled", "true");
|
|
}
|
|
});
|
|
|
|
insertLocation.insertAdjacentElement("afterend", pasteAndGo);
|
|
}
|
|
|
|
/**
|
|
* This notifies observers that the user has entered or selected something in
|
|
* the URL bar which will cause navigation.
|
|
*
|
|
* We use the observer service, so that we don't need to load extra facilities
|
|
* if they aren't being used, e.g. WebNavigation.
|
|
*
|
|
* @param {UrlbarResult} result
|
|
* Details of the result that was selected, if any.
|
|
*/
|
|
_notifyStartNavigation(result) {
|
|
Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
|
|
}
|
|
|
|
/**
|
|
* Returns a search mode object if a result should enter search mode when
|
|
* selected.
|
|
*
|
|
* @param {UrlbarResult} result
|
|
* The result to check.
|
|
* @param {string} [entry]
|
|
* If provided, this will be recorded as the entry point into search mode.
|
|
* See setSearchMode() documentation for details.
|
|
* @returns {object} A search mode object. Null if search mode should not be
|
|
* entered. See setSearchMode documentation for details.
|
|
*/
|
|
_searchModeForResult(result, entry = null) {
|
|
// Search mode is determined by the result's keyword or engine.
|
|
if (!result.payload.keyword && !result.payload.engine) {
|
|
return null;
|
|
}
|
|
|
|
let searchMode = lazy.UrlbarUtils.searchModeForToken(
|
|
result.payload.keyword
|
|
);
|
|
// If result.originalEngine is set, then the user is Alt+Tabbing
|
|
// through the one-offs, so the keyword doesn't match the engine.
|
|
if (
|
|
!searchMode &&
|
|
result.payload.engine &&
|
|
(!result.payload.originalEngine ||
|
|
result.payload.engine == result.payload.originalEngine)
|
|
) {
|
|
searchMode = { engineName: result.payload.engine };
|
|
}
|
|
|
|
if (searchMode) {
|
|
if (entry) {
|
|
searchMode.entry = entry;
|
|
} else {
|
|
switch (result.providerName) {
|
|
case "UrlbarProviderTopSites":
|
|
searchMode.entry = "topsites_urlbar";
|
|
break;
|
|
case "TabToSearch":
|
|
if (result.payload.dynamicType) {
|
|
searchMode.entry = "tabtosearch_onboard";
|
|
} else {
|
|
searchMode.entry = "tabtosearch";
|
|
}
|
|
break;
|
|
default:
|
|
searchMode.entry = "keywordoffer";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return searchMode;
|
|
}
|
|
|
|
/**
|
|
* Updates the UI so that search mode is either entered or exited.
|
|
*
|
|
* @param {object} searchMode
|
|
* See setSearchMode documentation. If null, then search mode is exited.
|
|
*/
|
|
_updateSearchModeUI(searchMode) {
|
|
let { engineName, source, isGeneralPurposeEngine } = searchMode || {};
|
|
|
|
// As an optimization, bail if the given search mode is null but search mode
|
|
// is already inactive. Otherwise browser_preferences_usage.js fails due to
|
|
// accessing the browser.urlbar.placeholderName pref (via the call to
|
|
// BrowserSearch.initPlaceHolder below) too many times. That test does not
|
|
// enter search mode, but it triggers many calls to this method with a null
|
|
// search mode, via setURI.
|
|
if (!engineName && !source && !this.hasAttribute("searchmode")) {
|
|
return;
|
|
}
|
|
|
|
this._searchModeIndicatorTitle.textContent = "";
|
|
this._searchModeLabel.textContent = "";
|
|
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
|
|
this._searchModeLabel.removeAttribute("data-l10n-id");
|
|
this.removeAttribute("searchmodesource");
|
|
|
|
if (!engineName && !source) {
|
|
try {
|
|
// This will throw before DOMContentLoaded in
|
|
// PrivateBrowsingUtils.privacyContextFromWindow because
|
|
// aWindow.docShell is null.
|
|
this.window.BrowserSearch.initPlaceHolder(true);
|
|
} catch (ex) {}
|
|
this.removeAttribute("searchmode");
|
|
return;
|
|
}
|
|
|
|
if (engineName) {
|
|
// Set text content for the search mode indicator.
|
|
this._searchModeIndicatorTitle.textContent = engineName;
|
|
this._searchModeLabel.textContent = engineName;
|
|
this.document.l10n.setAttributes(
|
|
this.inputField,
|
|
isGeneralPurposeEngine
|
|
? "urlbar-placeholder-search-mode-web-2"
|
|
: "urlbar-placeholder-search-mode-other-engine",
|
|
{ name: engineName }
|
|
);
|
|
} else if (source) {
|
|
let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
|
|
let l10nID = `urlbar-search-mode-${sourceName}`;
|
|
this.document.l10n.setAttributes(this._searchModeIndicatorTitle, l10nID);
|
|
this.document.l10n.setAttributes(this._searchModeLabel, l10nID);
|
|
this.document.l10n.setAttributes(
|
|
this.inputField,
|
|
`urlbar-placeholder-search-mode-other-${sourceName}`
|
|
);
|
|
this.setAttribute("searchmodesource", sourceName);
|
|
}
|
|
|
|
this.toggleAttribute("searchmode", true);
|
|
// Clear autofill.
|
|
if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
|
|
this.value = this.window.gBrowser.userTypedValue;
|
|
}
|
|
// Search mode should only be active when pageproxystate is invalid.
|
|
if (this.getAttribute("pageproxystate") == "valid") {
|
|
this.value = "";
|
|
this.setPageProxyState("invalid", true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if we should select all the text in the Urlbar based on the
|
|
* Urlbar state, and whether the selection is empty.
|
|
*/
|
|
_maybeSelectAll() {
|
|
if (
|
|
!this._preventClickSelectsAll &&
|
|
this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING &&
|
|
this.document.activeElement == this.inputField &&
|
|
this.inputField.selectionStart == this.inputField.selectionEnd
|
|
) {
|
|
this.select();
|
|
}
|
|
}
|
|
|
|
// Event handlers below.
|
|
|
|
_on_command(event) {
|
|
// Something is executing a command, likely causing a focus change. This
|
|
// should not be recorded as an abandonment. If the user is selecting a
|
|
// result menu item or entering search mode from a one-off, then they are
|
|
// in the same engagement and we should not discard.
|
|
if (
|
|
!event.target.classList.contains("urlbarView-result-menuitem") &&
|
|
(!event.target.classList.contains("searchbar-engine-one-off-item") ||
|
|
this.searchMode?.entry != "oneoff")
|
|
) {
|
|
this.controller.engagementEvent.discard();
|
|
}
|
|
}
|
|
|
|
_on_blur(event) {
|
|
// We cannot count every blur events after a missed engagement as abandoment
|
|
// because the user may have clicked on some view element that executes
|
|
// a command causing a focus change. For example opening preferences from
|
|
// the oneoff settings button.
|
|
// For now we detect that case by discarding the event on command, but we
|
|
// may want to figure out a more robust way to detect abandonment.
|
|
this.controller.engagementEvent.record(event, {
|
|
searchString: this._lastSearchString,
|
|
searchSource: this.getSearchSource(event),
|
|
});
|
|
|
|
this.focusedViaMousedown = false;
|
|
this._handoffSession = undefined;
|
|
this._isHandoffSession = false;
|
|
this.removeAttribute("focused");
|
|
|
|
if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
|
|
// If we were autofilling, remove the autofilled portion, by restoring
|
|
// the value to the last typed one.
|
|
this.value = this.window.gBrowser.userTypedValue;
|
|
} else if (this.value == this._focusUntrimmedValue) {
|
|
// If the value was untrimmed by _on_focus and didn't change, trim it.
|
|
this.value = this._focusUntrimmedValue;
|
|
}
|
|
this._focusUntrimmedValue = null;
|
|
|
|
this.formatValue();
|
|
this._resetSearchState();
|
|
|
|
// In certain cases, like holding an override key and confirming an entry,
|
|
// we don't key a keyup event for the override key, thus we make this
|
|
// additional cleanup on blur.
|
|
this._clearActionOverride();
|
|
|
|
// The extension input sessions depends more on blur than on the fact we
|
|
// actually cancel a running query, so we do it here.
|
|
if (lazy.ExtensionSearchHandler.hasActiveInputSession()) {
|
|
lazy.ExtensionSearchHandler.handleInputCancelled();
|
|
}
|
|
|
|
// Respect the autohide preference for easier inspecting/debugging via
|
|
// the browser toolbox.
|
|
if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
|
|
this.view.close();
|
|
}
|
|
|
|
if (this._revertOnBlurValue == this.value) {
|
|
this.handleRevert();
|
|
}
|
|
this._revertOnBlurValue = null;
|
|
|
|
// If there were search terms shown in the URL bar and the user
|
|
// didn't end up modifying the userTypedValue while it was
|
|
// focused, change back to a valid pageproxystate.
|
|
if (
|
|
this.window.gBrowser.selectedBrowser.searchTerms &&
|
|
this.window.gBrowser.userTypedValue == null
|
|
) {
|
|
this.setPageProxyState("valid", true);
|
|
}
|
|
|
|
// We may have hidden popup notifications, show them again if necessary.
|
|
if (
|
|
this.getAttribute("pageproxystate") != "valid" &&
|
|
this.window.UpdatePopupNotificationsVisibility
|
|
) {
|
|
this.window.UpdatePopupNotificationsVisibility();
|
|
}
|
|
|
|
// If user move the focus to another component while pressing Enter key,
|
|
// then keyup at that component, as we can't get the event, clear the promise.
|
|
if (this._keyDownEnterDeferred) {
|
|
this._keyDownEnterDeferred.resolve();
|
|
this._keyDownEnterDeferred = null;
|
|
}
|
|
this._isKeyDownWithCtrl = false;
|
|
|
|
Services.obs.notifyObservers(null, "urlbar-blur");
|
|
}
|
|
|
|
_on_click(event) {
|
|
if (
|
|
event.target == this.inputField ||
|
|
event.target == this._inputContainer ||
|
|
event.target.id == SEARCH_BUTTON_ID
|
|
) {
|
|
this._maybeSelectAll();
|
|
}
|
|
|
|
if (event.target == this._searchModeIndicatorClose && event.button != 2) {
|
|
this.searchMode = null;
|
|
this.view.oneOffSearchButtons.selectedButton = null;
|
|
if (this.view.isOpen) {
|
|
this.startQuery({
|
|
event,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
_on_contextmenu(event) {
|
|
this.addSearchEngineHelper.refreshContextMenu(event);
|
|
|
|
// Context menu opened via keyboard shortcut.
|
|
if (!event.button) {
|
|
return;
|
|
}
|
|
|
|
this._maybeSelectAll();
|
|
}
|
|
|
|
_on_focus(event) {
|
|
if (!this._hideFocus) {
|
|
this.setAttribute("focused", "true");
|
|
}
|
|
|
|
// When the search term matches the SERP, the URL bar is in a valid
|
|
// pageproxystate. In order to only show the search icon, switch to
|
|
// an invalid pageproxystate.
|
|
if (this.window.gBrowser.selectedBrowser.searchTerms) {
|
|
this.setPageProxyState("invalid", true);
|
|
}
|
|
|
|
// If the value was trimmed, check whether we should untrim it.
|
|
// This is necessary when a protocol was typed, but the whole url has
|
|
// invalid parts, like the origin, then editing and confirming the trimmed
|
|
// value would execute a search instead of visiting the typed url.
|
|
if (this.value != this._untrimmedValue) {
|
|
let untrim = false;
|
|
let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
|
|
if (fixedURI) {
|
|
try {
|
|
let expectedURI = Services.io.newURI(this._untrimmedValue);
|
|
untrim = fixedURI.displaySpec != expectedURI.displaySpec;
|
|
} catch (ex) {
|
|
untrim = true;
|
|
}
|
|
}
|
|
if (untrim) {
|
|
this.inputField.value = this._focusUntrimmedValue = this._untrimmedValue;
|
|
}
|
|
}
|
|
|
|
if (this.focusedViaMousedown) {
|
|
this.view.autoOpen({ event });
|
|
} else if (this.inputField.hasAttribute("refocused-by-panel")) {
|
|
this._maybeSelectAll();
|
|
}
|
|
|
|
this._updateUrlTooltip();
|
|
this.formatValue();
|
|
|
|
// Hide popup notifications, to reduce visual noise.
|
|
if (
|
|
this.getAttribute("pageproxystate") != "valid" &&
|
|
this.window.UpdatePopupNotificationsVisibility
|
|
) {
|
|
this.window.UpdatePopupNotificationsVisibility();
|
|
}
|
|
|
|
Services.obs.notifyObservers(null, "urlbar-focus");
|
|
}
|
|
|
|
_on_mouseover(event) {
|
|
this._updateUrlTooltip();
|
|
}
|
|
|
|
_on_draggableregionleftmousedown(event) {
|
|
if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
|
|
this.view.close();
|
|
}
|
|
}
|
|
|
|
_on_mousedown(event) {
|
|
switch (event.currentTarget) {
|
|
case this.textbox:
|
|
this._mousedownOnUrlbarDescendant = true;
|
|
|
|
if (
|
|
event.target != this.inputField &&
|
|
event.target != this._inputContainer &&
|
|
event.target.id != SEARCH_BUTTON_ID
|
|
) {
|
|
break;
|
|
}
|
|
|
|
this.focusedViaMousedown = !this.focused;
|
|
this._preventClickSelectsAll = this.focused;
|
|
|
|
// Keep the focus status, since the attribute may be changed
|
|
// upon calling this.focus().
|
|
const hasFocus = this.hasAttribute("focused");
|
|
if (event.target != this.inputField) {
|
|
this.focus();
|
|
}
|
|
|
|
// The rest of this case only cares about left clicks.
|
|
if (event.button != 0) {
|
|
break;
|
|
}
|
|
|
|
// Clear any previous selection unless we are focused, to ensure it
|
|
// doesn't affect drag selection.
|
|
if (this.focusedViaMousedown) {
|
|
this.inputField.setSelectionRange(0, 0);
|
|
}
|
|
|
|
if (event.target.id == SEARCH_BUTTON_ID) {
|
|
this._preventClickSelectsAll = true;
|
|
this.search(lazy.UrlbarTokenizer.RESTRICT.SEARCH);
|
|
} else {
|
|
// Do not suppress the focus border if we are already focused. If we
|
|
// did, we'd hide the focus border briefly then show it again if the
|
|
// user has Top Sites disabled, creating a flashing effect.
|
|
this.view.autoOpen({
|
|
event,
|
|
suppressFocusBorder: !hasFocus,
|
|
});
|
|
}
|
|
break;
|
|
case this.window:
|
|
if (this._mousedownOnUrlbarDescendant) {
|
|
this._mousedownOnUrlbarDescendant = false;
|
|
break;
|
|
}
|
|
// Don't close the view when clicking on a tab; we may want to keep the
|
|
// view open on tab switch, and the TabSelect event arrived earlier.
|
|
if (event.target.closest("tab")) {
|
|
break;
|
|
}
|
|
|
|
// Close the view when clicking on toolbars and other UI pieces that
|
|
// might not automatically remove focus from the input.
|
|
// Respect the autohide preference for easier inspecting/debugging via
|
|
// the browser toolbox.
|
|
if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
|
|
if (this.view.isOpen && !this.hasAttribute("focused")) {
|
|
// In this case, as blur event never happen from the inputField, we
|
|
// record abandonment event explicitly.
|
|
let blurEvent = new FocusEvent("blur", {
|
|
relatedTarget: this.inputField,
|
|
});
|
|
this.controller.engagementEvent.record(blurEvent, {
|
|
searchString: this._lastSearchString,
|
|
searchSource: this.getSearchSource(blurEvent),
|
|
});
|
|
}
|
|
|
|
this.view.close();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
_on_input(event) {
|
|
if (
|
|
this._autofillPlaceholder &&
|
|
this.value === this.window.gBrowser.userTypedValue &&
|
|
(event.inputType === "deleteContentBackward" ||
|
|
event.inputType === "deleteContentForward")
|
|
) {
|
|
// Take a telemetry if user deleted whole autofilled value.
|
|
Services.telemetry.scalarAdd("urlbar.autofill_deletion", 1);
|
|
}
|
|
|
|
let value = this.value;
|
|
this.valueIsTyped = true;
|
|
this._untrimmedValue = value;
|
|
this._resultForCurrentValue = null;
|
|
|
|
this.window.gBrowser.userTypedValue = value;
|
|
// Unset userSelectionBehavior because the user is modifying the search
|
|
// string, thus there's no valid selection. This is also used by the view
|
|
// to set "aria-activedescendant", thus it should never get stale.
|
|
this.controller.userSelectionBehavior = "none";
|
|
|
|
let compositionState = this._compositionState;
|
|
let compositionClosedPopup = this._compositionClosedPopup;
|
|
|
|
// Clear composition values if we're no more composing.
|
|
if (this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
|
|
this._compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
|
|
this._compositionClosedPopup = false;
|
|
}
|
|
|
|
if (value) {
|
|
this.setAttribute("usertyping", "true");
|
|
} else {
|
|
this.removeAttribute("usertyping");
|
|
}
|
|
this.removeAttribute("actiontype");
|
|
|
|
if (
|
|
this.getAttribute("pageproxystate") == "valid" &&
|
|
this.value != this._lastValidURLStr
|
|
) {
|
|
this.setPageProxyState("invalid", true);
|
|
}
|
|
|
|
if (!this.view.isOpen) {
|
|
this.view.clear();
|
|
} else if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) {
|
|
this.view.clear();
|
|
if (!this.searchMode || !this.view.oneOffSearchButtons.hasView) {
|
|
this.view.close();
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.view.removeAccessibleFocus();
|
|
|
|
// During composition with an IME, the following events happen in order:
|
|
// 1. a compositionstart event
|
|
// 2. some input events
|
|
// 3. a compositionend event
|
|
// 4. an input event
|
|
|
|
// We should do nothing during composition or if composition was canceled
|
|
// and we didn't close the popup on composition start.
|
|
if (
|
|
!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") &&
|
|
(compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING ||
|
|
(compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED &&
|
|
!compositionClosedPopup))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Autofill only when text is inserted (i.e., event.data is not empty) and
|
|
// it's not due to pasting.
|
|
const allowAutofill =
|
|
(!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") ||
|
|
compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) &&
|
|
!!event.data &&
|
|
!lazy.UrlbarUtils.isPasteEvent(event) &&
|
|
this._maybeAutofillPlaceholder(value);
|
|
|
|
this.startQuery({
|
|
searchString: value,
|
|
allowAutofill,
|
|
resetSearchState: false,
|
|
event,
|
|
});
|
|
}
|
|
|
|
_on_select(event) {
|
|
// On certain user input, AutoCopyListener::OnSelectionChange() updates
|
|
// the primary selection with user-selected text (when supported).
|
|
// Selection::NotifySelectionListeners() then dispatches a "select" event
|
|
// under similar conditions via TextInputListener::OnSelectionChange().
|
|
// This event is received here in order to replace the primary selection
|
|
// from the editor with text having the adjustments of
|
|
// _getSelectedValueForClipboard(), such as adding the scheme for the url.
|
|
//
|
|
// Other "select" events are also received, however, and must be excluded.
|
|
if (
|
|
// _suppressPrimaryAdjustment is set during select(). Don't update
|
|
// the primary selection because that is not the intent of user input,
|
|
// which may be new tab or urlbar focus.
|
|
this._suppressPrimaryAdjustment ||
|
|
// The check on isHandlingUserInput filters out async "select" events
|
|
// from setSelectionRange(), which occur when autofill text is selected.
|
|
!this.window.windowUtils.isHandlingUserInput ||
|
|
!Services.clipboard.isClipboardTypeSupported(
|
|
Services.clipboard.kSelectionClipboard
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let val = this._getSelectedValueForClipboard();
|
|
if (!val) {
|
|
return;
|
|
}
|
|
|
|
lazy.ClipboardHelper.copyStringToClipboard(
|
|
val,
|
|
Services.clipboard.kSelectionClipboard
|
|
);
|
|
}
|
|
|
|
_on_overflow(event) {
|
|
const targetIsPlaceholder =
|
|
event.originalTarget.implementedPseudoElement == "::placeholder";
|
|
// We only care about the non-placeholder text.
|
|
// This shouldn't be needed, see bug 1487036.
|
|
if (targetIsPlaceholder) {
|
|
return;
|
|
}
|
|
this._overflowing = true;
|
|
this.updateTextOverflow();
|
|
}
|
|
|
|
_on_underflow(event) {
|
|
const targetIsPlaceholder =
|
|
event.originalTarget.implementedPseudoElement == "::placeholder";
|
|
// We only care about the non-placeholder text.
|
|
// This shouldn't be needed, see bug 1487036.
|
|
if (targetIsPlaceholder) {
|
|
return;
|
|
}
|
|
this._overflowing = false;
|
|
|
|
this.updateTextOverflow();
|
|
|
|
this._updateUrlTooltip();
|
|
}
|
|
|
|
_on_paste(event) {
|
|
let originalPasteData = event.clipboardData.getData("text/plain");
|
|
if (!originalPasteData) {
|
|
return;
|
|
}
|
|
|
|
let oldValue = this.inputField.value;
|
|
let oldStart = oldValue.substring(0, this.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.selectionEnd);
|
|
|
|
let fixedURI, keywordAsSent;
|
|
try {
|
|
({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo(
|
|
originalPasteData,
|
|
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
|
|
Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
|
|
));
|
|
} catch (e) {}
|
|
|
|
let pasteData;
|
|
if (keywordAsSent) {
|
|
// For only keywords, replace any white spaces including line break with
|
|
// white space.
|
|
pasteData = originalPasteData.replace(/\s/g, " ");
|
|
} else if (
|
|
fixedURI?.scheme === "data" &&
|
|
!fixedURI.spec.match(/^data:.+;base64,/)
|
|
) {
|
|
// For data url without base64, replace line break with white space.
|
|
pasteData = originalPasteData.replace(/[\r\n]/g, " ");
|
|
} else {
|
|
// For normal url or data url having basic64, or if fixup failed, just
|
|
// remove line breaks.
|
|
pasteData = originalPasteData.replace(/[\r\n]/g, "");
|
|
}
|
|
|
|
pasteData = lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData);
|
|
|
|
if (originalPasteData != pasteData) {
|
|
// Unfortunately we're not allowed to set the bits being pasted
|
|
// so cancel this event:
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
|
|
this.inputField.value = oldStart + pasteData + oldEnd;
|
|
this._untrimmedValue = this.inputField.value;
|
|
|
|
if (this._untrimmedValue) {
|
|
this.setAttribute("usertyping", "true");
|
|
} else {
|
|
this.removeAttribute("usertyping");
|
|
}
|
|
|
|
// Fix up cursor/selection:
|
|
let newCursorPos = oldStart.length + pasteData.length;
|
|
this.inputField.setSelectionRange(newCursorPos, newCursorPos);
|
|
|
|
this.startQuery({
|
|
searchString: this.inputField.value,
|
|
allowAutofill: false,
|
|
resetSearchState: false,
|
|
event,
|
|
});
|
|
}
|
|
}
|
|
|
|
_on_scrollend(event) {
|
|
this.updateTextOverflow();
|
|
}
|
|
|
|
_on_TabSelect(event) {
|
|
this._gotTabSelect = true;
|
|
this._afterTabSelectAndFocusChange();
|
|
}
|
|
|
|
_on_beforeinput(event) {
|
|
if (event.data && this._keyDownEnterDeferred) {
|
|
// Ignore char key input while processing enter key.
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
_on_keydown(event) {
|
|
if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
|
|
if (this._keyDownEnterDeferred) {
|
|
this._keyDownEnterDeferred.reject();
|
|
}
|
|
this._keyDownEnterDeferred = lazy.PromiseUtils.defer();
|
|
event._disableCanonization = this._isKeyDownWithCtrl;
|
|
} else if (event.keyCode !== KeyEvent.DOM_VK_CONTROL && event.ctrlKey) {
|
|
this._isKeyDownWithCtrl = true;
|
|
}
|
|
|
|
// Due to event deferring, it's possible preventDefault() won't be invoked
|
|
// soon enough to actually prevent some of the default behaviors, thus we
|
|
// have to handle the event "twice". This first immediate call passes false
|
|
// as second argument so that handleKeyNavigation will only simulate the
|
|
// event handling, without actually executing actions.
|
|
// TODO (Bug 1541806): improve this handling, maybe by delaying actions
|
|
// instead of events.
|
|
if (this.eventBufferer.shouldDeferEvent(event)) {
|
|
this.controller.handleKeyNavigation(event, false);
|
|
}
|
|
this._toggleActionOverride(event);
|
|
this.eventBufferer.maybeDeferEvent(event, () => {
|
|
this.controller.handleKeyNavigation(event);
|
|
});
|
|
}
|
|
|
|
async _on_keyup(event) {
|
|
if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
|
|
this._isKeyDownWithCtrl = false;
|
|
}
|
|
|
|
this._toggleActionOverride(event);
|
|
|
|
// Pressing Enter key while pressing Meta key, and next, even when releasing
|
|
// Enter key before releasing Meta key, the keyup event is not fired.
|
|
// Therefore, if Enter keydown is detecting, continue the post processing
|
|
// for Enter key when any keyup event is detected.
|
|
if (this._keyDownEnterDeferred) {
|
|
if (this._keyDownEnterDeferred.loadedContent) {
|
|
try {
|
|
const loadingBrowser = await this._keyDownEnterDeferred.promise;
|
|
// Ensure the selected browser didn't change in the meanwhile.
|
|
if (this.window.gBrowser.selectedBrowser === loadingBrowser) {
|
|
loadingBrowser.focus();
|
|
// Make sure the domain name stays visible for spoof protection and usability.
|
|
this.inputField.setSelectionRange(0, 0);
|
|
}
|
|
} catch (ex) {
|
|
// Not all the Enter actions in the urlbar will cause a navigation, then it
|
|
// is normal for this to be rejected.
|
|
// If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here
|
|
// to ensure not overwriting the new value created by keydown.
|
|
}
|
|
} else {
|
|
// Discard the _keyDownEnterDeferred promise to receive any key inputs immediately.
|
|
this._keyDownEnterDeferred.resolve();
|
|
}
|
|
|
|
this._keyDownEnterDeferred = null;
|
|
}
|
|
}
|
|
|
|
_on_compositionstart(event) {
|
|
if (this._compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
|
|
throw new Error("Trying to start a nested composition?");
|
|
}
|
|
this._compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING;
|
|
|
|
if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
|
|
return;
|
|
}
|
|
|
|
// Close the view. This will also stop searching.
|
|
if (this.view.isOpen) {
|
|
// We're closing the view, but we want to retain search mode if the
|
|
// selected result was previewing it.
|
|
if (this.searchMode) {
|
|
// If we entered search mode with an empty string, clear userTypedValue,
|
|
// otherwise confirmSearchMode may try to set it as value.
|
|
// This can happen for example if we entered search mode typing a
|
|
// a partial engine domain and selecting a tab-to-search result.
|
|
if (!this.value) {
|
|
this.window.gBrowser.userTypedValue = null;
|
|
}
|
|
this.confirmSearchMode();
|
|
}
|
|
this._compositionClosedPopup = true;
|
|
this.view.close();
|
|
} else {
|
|
this._compositionClosedPopup = false;
|
|
}
|
|
}
|
|
|
|
_on_compositionend(event) {
|
|
if (this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
|
|
throw new Error("Trying to stop a non existing composition?");
|
|
}
|
|
|
|
if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
|
|
// Clear the selection and the cached result, since they refer to the
|
|
// state before this composition. A new input even will be generated
|
|
// after this.
|
|
this.view.clearSelection();
|
|
this._resultForCurrentValue = null;
|
|
}
|
|
|
|
// We can't yet retrieve the committed value from the editor, since it isn't
|
|
// completely committed yet. We'll handle it at the next input event.
|
|
this._compositionState = event.data
|
|
? lazy.UrlbarUtils.COMPOSITION.COMMIT
|
|
: lazy.UrlbarUtils.COMPOSITION.CANCELED;
|
|
}
|
|
|
|
_on_dragstart(event) {
|
|
// Drag only if the gesture starts from the input field.
|
|
let nodePosition = this.inputField.compareDocumentPosition(
|
|
event.originalTarget
|
|
);
|
|
if (
|
|
event.target != this.inputField &&
|
|
!(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Don't cover potential drop targets on the toolbars or in content.
|
|
this.view.close();
|
|
|
|
// Only customize the drag data if the entire value is selected and it's a
|
|
// loaded URI. Use default behavior otherwise.
|
|
if (
|
|
this.selectionStart != 0 ||
|
|
this.selectionEnd != this.inputField.textLength ||
|
|
this.getAttribute("pageproxystate") != "valid"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let uri = this.makeURIReadable(this.window.gBrowser.currentURI);
|
|
let href = uri.displaySpec;
|
|
let title = this.window.gBrowser.contentTitle || href;
|
|
|
|
event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
|
|
event.dataTransfer.setData("text/plain", href);
|
|
event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
|
|
event.dataTransfer.effectAllowed = "copyLink";
|
|
event.stopPropagation();
|
|
}
|
|
|
|
_on_dragover(event) {
|
|
if (!getDroppableData(event)) {
|
|
event.dataTransfer.dropEffect = "none";
|
|
}
|
|
}
|
|
|
|
_on_drop(event) {
|
|
let droppedItem = getDroppableData(event);
|
|
let droppedURL = URL.isInstance(droppedItem)
|
|
? droppedItem.href
|
|
: droppedItem;
|
|
if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) {
|
|
let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event);
|
|
this.value = droppedURL;
|
|
this.setPageProxyState("invalid");
|
|
this.focus();
|
|
// To simplify tracking of events, register an initial event for event
|
|
// telemetry, to replace the missing input event.
|
|
this.controller.engagementEvent.start(event);
|
|
this.controller.clearLastQueryContextCache();
|
|
this.handleNavigation({ triggeringPrincipal: principal });
|
|
// For safety reasons, in the drop case we don't want to immediately show
|
|
// the the dropped value, instead we want to keep showing the current page
|
|
// url until an onLocationChange happens.
|
|
// See the handling in `setURI` for further details.
|
|
this.window.gBrowser.userTypedValue = null;
|
|
this.setURI(null, true);
|
|
}
|
|
}
|
|
|
|
_on_customizationstarting() {
|
|
this.blur();
|
|
|
|
this.inputField.controllers.removeController(this._copyCutController);
|
|
delete this._copyCutController;
|
|
}
|
|
|
|
_on_aftercustomization() {
|
|
this._initCopyCutController();
|
|
this._initPasteAndGo();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tries to extract droppable data from a DND event.
|
|
*
|
|
* @param {Event} event The DND event to examine.
|
|
* @returns {URL|string|null}
|
|
* null if there's a security reason for which we should do nothing.
|
|
* A URL object if it's a value we can load.
|
|
* A string value otherwise.
|
|
*/
|
|
function getDroppableData(event) {
|
|
let links;
|
|
try {
|
|
links = Services.droppedLinkHandler.dropLinks(event);
|
|
} catch (ex) {
|
|
// This is either an unexpected failure or a security exception; in either
|
|
// case we should always return null.
|
|
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 && links[0].url) {
|
|
event.preventDefault();
|
|
let href = links[0].url;
|
|
if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) {
|
|
// We may have stripped an unsafe protocol like javascript: and if so
|
|
// there's no point in handling a partial drop.
|
|
event.stopImmediatePropagation();
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// If this throws, checkLoadURStrWithPrincipal 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.
|
|
let url = new URL(href);
|
|
// If we succeed, try to pass security checks. If this works, return the
|
|
// URL object. If the *security checks* fail, return null.
|
|
try {
|
|
let principal = Services.droppedLinkHandler.getTriggeringPrincipal(
|
|
event
|
|
);
|
|
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
|
principal,
|
|
url.href,
|
|
Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
|
|
);
|
|
return url;
|
|
} catch (ex) {
|
|
return null;
|
|
}
|
|
} catch (ex) {
|
|
// We couldn't make a URL out of this. Continue on, and return text below.
|
|
}
|
|
}
|
|
// Handle as text.
|
|
return event.dataTransfer.getData("text/plain");
|
|
}
|
|
|
|
/**
|
|
* Decodes the given URI for displaying it in the address bar without losing
|
|
* information, such that hitting Enter again will load the same URI.
|
|
*
|
|
* @param {nsIURI} aURI
|
|
* The URI to decode
|
|
* @returns {string}
|
|
* The decoded URI
|
|
*/
|
|
function losslessDecodeURI(aURI) {
|
|
let scheme = aURI.scheme;
|
|
let value = aURI.displaySpec;
|
|
|
|
// Try to decode as UTF-8 if there's no encoding sequence that we would break.
|
|
if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
|
|
let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
|
|
if (decodeASCIIOnly) {
|
|
// This only decodes ascii characters (hex) 20-7e, except 25 (%).
|
|
// This avoids both cases stipulated below (%-related issues, and \r, \n
|
|
// and \t, which would be %0d, %0a and %09, respectively) as well as any
|
|
// non-US-ascii characters.
|
|
value = value.replace(
|
|
/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g,
|
|
decodeURI
|
|
);
|
|
} else {
|
|
try {
|
|
value = decodeURI(value)
|
|
// decodeURI decodes %25 to %, which creates unintended encoding
|
|
// sequences. Re-encode it, unless it's part of a sequence that
|
|
// survived decodeURI, i.e. one for:
|
|
// ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
|
|
// (RFC 3987 section 3.2)
|
|
.replace(
|
|
/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi,
|
|
encodeURIComponent
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
// Encode potentially invisible characters:
|
|
// U+0000-001F: C0/C1 control characters
|
|
// U+007F-009F: commands
|
|
// U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces
|
|
// U+2028-2029: line and paragraph separators
|
|
// U+2800: braille empty pattern
|
|
// U+FFFC: object replacement character
|
|
// Encode any trailing whitespace that may be part of a pasted URL, so that it
|
|
// doesn't get eaten away by the location bar (bug 410726).
|
|
// Encode all adjacent space chars (U+0020), to prevent spoofing attempts
|
|
// where they would push part of the URL to overflow the location bar
|
|
// (bug 1395508). A single space, or the last space if the are many, is
|
|
// preserved to maintain readability of certain urls. We only do this for the
|
|
// common space, because others may be eaten when copied to the clipboard, so
|
|
// it's safer to preserve them encoded.
|
|
value = value.replace(
|
|
// eslint-disable-next-line no-control-regex
|
|
/[\u0000-\u001f\u007f-\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u2800\u3000\ufffc]|[\r\n\t]|\u0020(?=\u0020)|\s$/g,
|
|
encodeURIComponent
|
|
);
|
|
|
|
// Encode characters that are ignorable, can't be rendered usefully, or may
|
|
// confuse users.
|
|
//
|
|
// Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded
|
|
// per bug 582186:
|
|
// U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
|
|
// U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
|
|
// U+FFF0-FFFB, U+1D173-1D17A (U+D834 + DD73-DD7A),
|
|
// U+E0000-E0FFF (U+DB40-DB43 + U+DC00-DFFF)
|
|
// Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
|
|
// U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
|
|
// Other format characters in the Cf category that are unlikely to be rendered
|
|
// usefully:
|
|
// U+0600-0605, U+08E2, U+110BD (U+D804 + U+DCBD),
|
|
// U+110CD (U+D804 + U+DCCD), U+13430-13438 (U+D80D + U+DC30-DC38),
|
|
// U+1BCA0-1BCA3 (U+D82F + U+DCA0-DCA3)
|
|
// Mimicking UI parts:
|
|
// U+1F50F-1F513 (U+D83D + U+DD0F-DD13), U+1F6E1 (U+D83D + U+DEE1)
|
|
value = value.replace(
|
|
// eslint-disable-next-line no-misleading-character-class
|
|
/[\u00ad\u034f\u061c\u06dd\u070f\u115f\u1160\u17b4\u17b5\u180b-\u180e\u200b\u200e\u200f\u202a-\u202e\u2060-\u206f\u3164\u0600-\u0605\u08e2\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufffb]|\ud804[\udcbd\udccd]|\ud80d[\udc30-\udc38]|\ud82f[\udca0-\udca3]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]|\ud83d[\udd0f-\udd13\udee1]/g,
|
|
encodeURIComponent
|
|
);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Handles copy and cut commands for the urlbar.
|
|
*/
|
|
class CopyCutController {
|
|
/**
|
|
* @param {UrlbarInput} urlbar
|
|
* The UrlbarInput instance to use this controller for.
|
|
*/
|
|
constructor(urlbar) {
|
|
this.urlbar = urlbar;
|
|
}
|
|
|
|
/**
|
|
* @param {string} command
|
|
* The name of the command to handle.
|
|
*/
|
|
doCommand(command) {
|
|
let urlbar = this.urlbar;
|
|
let val = urlbar._getSelectedValueForClipboard();
|
|
if (!val) {
|
|
return;
|
|
}
|
|
|
|
if (command == "cmd_cut" && this.isCommandEnabled(command)) {
|
|
let start = urlbar.selectionStart;
|
|
let end = urlbar.selectionEnd;
|
|
urlbar.inputField.value =
|
|
urlbar.inputField.value.substring(0, start) +
|
|
urlbar.inputField.value.substring(end);
|
|
urlbar.inputField.setSelectionRange(start, start);
|
|
|
|
let event = new UIEvent("input", {
|
|
bubbles: true,
|
|
cancelable: false,
|
|
view: urlbar.window,
|
|
detail: 0,
|
|
});
|
|
urlbar.inputField.dispatchEvent(event);
|
|
}
|
|
|
|
lazy.ClipboardHelper.copyString(val);
|
|
}
|
|
|
|
/**
|
|
* @param {string} command
|
|
* The name of the command to check.
|
|
* @returns {boolean}
|
|
* Whether the command is handled by this controller.
|
|
*/
|
|
supportsCommand(command) {
|
|
switch (command) {
|
|
case "cmd_copy":
|
|
case "cmd_cut":
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {string} command
|
|
* The name of the command to check.
|
|
* @returns {boolean}
|
|
* Whether the command should be enabled.
|
|
*/
|
|
isCommandEnabled(command) {
|
|
return (
|
|
this.supportsCommand(command) &&
|
|
(command != "cmd_cut" || !this.urlbar.readOnly) &&
|
|
this.urlbar.selectionStart < this.urlbar.selectionEnd
|
|
);
|
|
}
|
|
|
|
onEvent() {}
|
|
}
|
|
|
|
/**
|
|
* Manages the Add Search Engine contextual menu entries.
|
|
*
|
|
* Note: setEnginesFromBrowser must be invoked from the outside when the
|
|
* page provided engines list changes.
|
|
* refreshContextMenu must be invoked when the context menu is opened.
|
|
*/
|
|
class AddSearchEngineHelper {
|
|
/**
|
|
* @type {UrlbarSearchOneOffs}
|
|
*/
|
|
shortcutButtons;
|
|
|
|
/**
|
|
* @param {UrlbarInput} input The parent UrlbarInput.
|
|
*/
|
|
constructor(input) {
|
|
this.input = input;
|
|
this.shortcutButtons = input.view.oneOffSearchButtons;
|
|
}
|
|
|
|
/**
|
|
* If there's more than this number of engines, the context menu offers
|
|
* them in a submenu.
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
get maxInlineEngines() {
|
|
return this.shortcutButtons._maxInlineAddEngines;
|
|
}
|
|
|
|
/**
|
|
* Invoked by browser when the list of available engines changes.
|
|
*
|
|
* @param {object} browser The invoking browser.
|
|
*/
|
|
setEnginesFromBrowser(browser) {
|
|
this.browsingContext = browser.browsingContext;
|
|
// Make a copy of the array for state comparison.
|
|
let engines = browser.engines?.slice() || [];
|
|
if (!this._sameEngines(this.engines, engines)) {
|
|
this.engines = engines;
|
|
this.shortcutButtons.updateWebEngines(engines);
|
|
}
|
|
}
|
|
|
|
_sameEngines(engines1, engines2) {
|
|
if (engines1?.length != engines2?.length) {
|
|
return false;
|
|
}
|
|
return lazy.ObjectUtils.deepEqual(
|
|
engines1.map(e => e.title),
|
|
engines2.map(e => e.title)
|
|
);
|
|
}
|
|
|
|
_createMenuitem(engine, index) {
|
|
let elt = this.input.document.createXULElement("menuitem");
|
|
elt.setAttribute("anonid", `add-engine-${index}`);
|
|
elt.classList.add("menuitem-iconic");
|
|
elt.classList.add("context-menu-add-engine");
|
|
elt.setAttribute("data-l10n-id", "search-one-offs-add-engine");
|
|
elt.setAttribute(
|
|
"data-l10n-args",
|
|
JSON.stringify({ engineName: engine.title })
|
|
);
|
|
elt.setAttribute("uri", engine.uri);
|
|
if (engine.icon) {
|
|
elt.setAttribute("image", engine.icon);
|
|
} else {
|
|
elt.removeAttribute("image", engine.icon);
|
|
}
|
|
elt.addEventListener("command", this._onCommand.bind(this));
|
|
return elt;
|
|
}
|
|
|
|
_createMenu(engine) {
|
|
let elt = this.input.document.createXULElement("menu");
|
|
elt.setAttribute("anonid", "add-engine-menu");
|
|
elt.classList.add("menu-iconic");
|
|
elt.classList.add("context-menu-add-engine");
|
|
elt.setAttribute("data-l10n-id", "search-one-offs-add-engine-menu");
|
|
if (engine.icon) {
|
|
elt.setAttribute("image", engine.icon);
|
|
}
|
|
let popup = this.input.document.createXULElement("menupopup");
|
|
elt.appendChild(popup);
|
|
return elt;
|
|
}
|
|
|
|
refreshContextMenu() {
|
|
let engines = this.engines;
|
|
|
|
// Certain operations, like customization, destroy and recreate widgets,
|
|
// so we cannot rely on cached elements.
|
|
if (!this.input.querySelector(".menuseparator-add-engine")) {
|
|
this.contextSeparator = this.input.document.createXULElement(
|
|
"menuseparator"
|
|
);
|
|
this.contextSeparator.setAttribute("anonid", "add-engine-separator");
|
|
this.contextSeparator.classList.add("menuseparator-add-engine");
|
|
this.contextSeparator.collapsed = true;
|
|
let contextMenu = this.input.querySelector("moz-input-box").menupopup;
|
|
contextMenu.appendChild(this.contextSeparator);
|
|
}
|
|
|
|
this.contextSeparator.collapsed = !engines.length;
|
|
let curElt = this.contextSeparator;
|
|
// Remove the previous items, if any.
|
|
for (let elt = curElt.nextElementSibling; elt; ) {
|
|
let nextElementSibling = elt.nextElementSibling;
|
|
elt.remove();
|
|
elt = nextElementSibling;
|
|
}
|
|
|
|
// If the page provides too many engines, we only show a single menu entry
|
|
// with engines in a submenu.
|
|
if (engines.length > this.maxInlineEngines) {
|
|
// Set the menu button's image to the image of the first engine. The
|
|
// offered engines may have differing images, so there's no perfect
|
|
// choice here.
|
|
let elt = this._createMenu(engines[0]);
|
|
this.contextSeparator.insertAdjacentElement("afterend", elt);
|
|
curElt = elt.lastElementChild;
|
|
}
|
|
|
|
// Insert the engines, either in the contextual menu or the sub menu.
|
|
for (let i = 0; i < engines.length; ++i) {
|
|
let elt = this._createMenuitem(engines[i], i);
|
|
if (curElt.localName == "menupopup") {
|
|
curElt.appendChild(elt);
|
|
} else {
|
|
curElt.insertAdjacentElement("afterend", elt);
|
|
}
|
|
curElt = elt;
|
|
}
|
|
}
|
|
|
|
async _onCommand(event) {
|
|
let added = await lazy.SearchUIUtils.addOpenSearchEngine(
|
|
event.target.getAttribute("uri"),
|
|
event.target.getAttribute("image"),
|
|
this.browsingContext
|
|
).catch(console.error);
|
|
if (added) {
|
|
// Remove the offered engine from the list. The browser updated the
|
|
// engines list at this point, so we just have to refresh the menu.)
|
|
this.refreshContextMenu();
|
|
}
|
|
}
|
|
}
|