Source code

Revision control

Copy as Markdown

Other Tools

/* 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",
BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
ExtensionSearchHandler:
"resource://gre/modules/ExtensionSearchHandler.sys.mjs",
ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
SearchModeSwitcher: "resource:///modules/SearchModeSwitcher.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",
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.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",
UrlbarSearchTermsPersistence:
"resource:///modules/UrlbarSearchTermsPersistence.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"ClipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"QueryStringStripper",
"@mozilla.org/url-query-string-stripper;1",
"nsIURLQueryStringStripper"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"QUERY_STRIPPING_STRIP_ON_SHARE",
"privacy.query_stripping.strip_on_share.enabled",
false
);
const DEFAULT_FORM_HISTORY_NAME = "searchbar-history";
const SEARCH_BUTTON_CLASS = "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.controller = new lazy.UrlbarController({
input: this,
eventTelemetryCategory: options.eventTelemetryCategory,
});
this.view = new lazy.UrlbarView(this);
this.searchModeSwitcher = new lazy.SearchModeSwitcher(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 = "";
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");
ChromeUtils.defineLazyGetter(this, "valueFormatter", () => {
return new lazy.UrlbarValueFormatter(this);
});
ChromeUtils.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",
"selectionchange",
];
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.textbox.addEventListener("mouseup", 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();
this._initStripOnShare();
// Tracks IME composition.
this._compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
this._compositionClosedPopup = false;
this.editor.newlineHandling =
Ci.nsIEditor.eNewlinesStripSurroundingWhitespace;
ChromeUtils.defineLazyGetter(this, "logger", () =>
lazy.UrlbarUtils.getLogger({ prefix: "Input" })
);
}
/**
* 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;
}
setSelectionRange(selectionStart, selectionEnd) {
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.setSelectionRange(selectionStart, selectionEnd);
this._suppressPrimaryAdjustment = false;
}
saveSelectionStateForBrowser(browser) {
let state = this.getBrowserState(browser);
state.selection = {
start: this.selectionStart,
end: this.selectionEnd,
// When restoring a URI from an empty value, we don't want to untrim it.
shouldUntrim: this.value && !this._protocolIsTrimmed,
};
}
restoreSelectionStateForBrowser(browser) {
// Address bar must be focused to untrim and for selection to make sense.
this.focus();
let state = this.getBrowserState(browser);
if (state.selection) {
if (state.selection.shouldUntrim) {
this.#maybeUntrimUrl();
}
this.setSelectionRange(state.selection.start, state.selection.end);
}
}
/**
* 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.UrlbarSearchTermsPersistence.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.#isOpenedPageInBlankTargetLoading ||
this.window.gBrowser.currentURI;
// Strip off usernames and passwords for the location bar
try {
uri = Services.io.createExposableURI(uri);
} catch (e) {}
let isInitialPageControlledByWebContent = false;
// 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 {
isInitialPageControlledByWebContent = true;
// 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") ||
isInitialPageControlledByWebContent);
}
} else if (
this.window.isInitialPage(value) &&
lazy.BrowserUIUtils.checkEmptyPageOrigin(
this.window.gBrowser.selectedBrowser
)
) {
value = "";
valid = true;
}
const previousUntrimmedValue = this.untrimmedValue;
// When calculating the selection indices we must take into account a
// trimmed protocol.
let offset = this._protocolIsTrimmed
? lazy.BrowserUIUtils.trimURLProtocol.length
: 0;
const previousSelectionStart = this.selectionStart + offset;
const previousSelectionEnd = this.selectionEnd + offset;
this.value = value;
this.valueIsTyped = !valid;
this.toggleAttribute("usertyping", !valid && value);
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 - offset,
previousSelectionEnd - offset
);
} 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 - offset,
previousSelectionEnd - offset
);
}
}
// 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 || { triggeringPrincipal };
// 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;
// Whether the user has been editing the value in the URL bar after selecting
// the result. However, if the result type is tip, pick as it is. The result
// heuristic is also kept the behavior as is for safety.
let safeToPickResult =
result &&
(result.heuristic ||
!this.valueIsTyped ||
result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP ||
this.value == this.#getValueFromResult(result));
if (
!isComposing &&
element &&
(!oneOffParams?.engine || selectedPrivateEngineResult) &&
safeToPickResult
) {
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);
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 || this._resultForCurrentValue || null,
});
let isValidUrl = false;
try {
new URL(url);
isValidUrl = true;
} catch (ex) {}
if (isValidUrl) {
// Annotate if the untrimmed value contained a scheme, to later potentially
// be upgraded by schemeless HTTPS-First.
openParams.wasSchemelessInput = this.#isSchemeless(this.untrimmedValue);
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, this.window)
.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(() => {
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,
keywordAsSent,
} = Services.uriFixup.getFixupURIInfo(url, flags);
if (
where != "current" ||
browser.lastLocationChange == lastLocationChange
) {
openParams.postData = postData;
if (!keywordAsSent) {
// `uri` is not a search engine url, so we annotate if the untrimmed
// value contained a scheme, to potentially be later upgraded by
// schemeless HTTPS-First.
openParams.wasSchemelessInput = this.#isSchemeless(
this.untrimmedValue
);
}
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
) {
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);
this.logger.debug(
`pickElement ${element} with event ${event?.type}, result: ${result}`
);
if (!result) {
return;
}
if (element?.dataset.action && element?.dataset.action != "tabswitch") {
this.controller.engagementEvent.record(event, {
result,
element,
searchString: this._lastSearchString,
selType: "action",
searchSource: this.getSearchSource(event),
});
this.view.close();
let provider = lazy.UrlbarProvidersManager.getActionProvider(
element.dataset.providerName
);
let { queryContext } = this.controller._lastQueryContextWrapper || {};
provider.pickAction(queryContext, this.controller, element);
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;
}
if (element?.dataset.command) {
this.#pickMenuResult(result, event, element, browser);
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;
}
let resultUrl = element?.dataset.url;
let originalUntrimmedValue = this.untrimmedValue;
let isCanonized = this.setValueFromResult({
result,
event,
urlOverride: resultUrl,
});
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 (
resultUrl &&
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._untrimmedValue, event, where, openParams, browser);
return;
}
let { url, postData } = resultUrl
? { url: resultUrl, postData: null }
: lazy.UrlbarUtils.getUrlFromResult(result);
openParams.postData = postData;
switch (result.type) {
case lazy.UrlbarUtils.RESULT_TYPE.URL: {
if (result.heuristic) {
// 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 (
lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
) {
url = originalUntrimmedValue;
}
// Annotate if the untrimmed value contained a scheme, to later potentially
// be upgraded by schemeless HTTPS-First.
openParams.wasSchemelessInput = this.#isSchemeless(
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: {
// Behaviour is reversed with SecondaryActions, default behaviour is to navigate
// and button is provided to switch to tab.
if (
this.hasAttribute("action-override") ||
(lazy.UrlbarPrefs.getScotchBonnetPref(
"secondaryActions.featureGate"
) &&
element?.dataset.action !== "tabswitch")
) {
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),
true,
loadOpts,
lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId(
result.payload.userContextId
)
? result.payload.userContextId
: null
);
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
);
}
// TODO (Bug 1865757): We should not show a "switchtotab" result for
// tabs that are not currently open. Find out why tabs are not being
// properly unregistered when they are being closed.
if (!switched) {
console.error(`Tried to switch to non existant tab: ${url}`);
lazy.UrlbarProviderOpenTabs.unregisterOpenTab(
url,
result.payload.userContextId,
this.isPrivate
);
}
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.sys.mjs 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,
};
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 = `${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;
}
case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: {
this.handleRevert();
this.maybeConfirmSearchModeFromResult({
result,
checkValue: false,
});
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
);
}
}
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;
}
// We won't allow trimming when calling _setValue, since it makes too easy
// for the user to wrongly transform `https` into `http`, for example by
// picking a https://site/path_1 result and editing the path to path_2,
// then we'd end up visiting http://site/path_2.
// Trimming `http` would be ok, but there's other cases where it's unsafe,
// like transforming a url into a search.
// This choice also makes it easier to copy the full url of a result.
// 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) {
this._setValue(canonizedUrl);
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) {
this._setValue(this.#getValueFromResult(result), {
actionType: this.#getActionTypeFromResult(result),
});
this.searchMode = null;
}
this.setResultForCurrentValue(result);
return false;
}
if (!result.autofill) {
this._setValue(this.#getValueFromResult(result, urlOverride), {
actionType: this.#getActionTypeFromResult(result),
});
}
this.setResultForCurrentValue(result);
// Update placeholder selection and value to the current selected result to
// prevent the on_selectionchange event to detect a "accent-character"
// insertion.
if (!result.autofill && this._autofillPlaceholder) {
this._autofillPlaceholder.value = this.value;
this._autofillPlaceholder.selectionStart = this.value.length;
this._autofillPlaceholder.selectionEnd = this.value.length;
}
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 });
}
/**
* Clears displayed autofill values and unsets the autofill placeholder.
*/
#clearAutofill() {
if (!this._autofillPlaceholder) {
return;
}
let currentSelectionStart = this.selectionStart;
let currentSelectionEnd = this.selectionEnd;
// Overriding this value clears the selection.
this.inputField.value = this.value.substring(
0,
this._autofillPlaceholder.selectionStart
);
this._autofillPlaceholder = null;
// Restore selection
this.setSelectionRange(currentSelectionStart, currentSelectionEnd);
}
/**
* 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);
}
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,
autofillIgnoresSelection = false,
searchString,
resetSearchState = true,
event,
} = {}) {
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");
}
let queryContext = this.#makeQueryContext({
allowAutofill,
event,
searchString,
});
if (event) {
this.controller.engagementEvent.start(event, queryContext, searchString);
}
if (this._suppressStartQuery) {
return;
}
this._autofillIgnoresSelection = autofillIgnoresSelection;
if (resetSearchState) {
this._resetSearchState();
}
if (this.searchMode) {
this.confirmSearchMode();
}
this._lastSearchString = searchString;
this._valueOnLastSearch = this.value;
// 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(queryContext);
}
/**
* 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.toggleAttribute("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.getBrowserState(browser).searchModes;
// 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