gecko-dev/browser/components/urlbar/UrlbarInput.jsm
Mark Banner 10cebf3c34 Bug 1515083 - Re-implement telemetry for selected index/type on QuantumBar. r=adw
This makes the browser_UsageTelemetry_urlbar*.js tests pass for the all of the
FX_URLBAR_SELECTED_RESULT_* histograms apart from the "METHOD" one which will be handled
in bug 1500476.

I have handled the recording of telemetry in the controller, as this seems a better
location than BrowserUsageTelemetry.jsm due to needing to reach into the results and obtain
specific details.

Differential Revision: https://phabricator.services.mozilla.com/D19785

--HG--
extra : moz-landing-system : lando
2019-02-15 14:57:23 +00:00

1145 lines
35 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["UrlbarInput"];
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
ReaderMode: "resource://gre/modules/ReaderMode.jsm",
Services: "resource://gre/modules/Services.jsm",
UrlbarController: "resource:///modules/UrlbarController.jsm",
UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.jsm",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm",
UrlbarView: "resource:///modules/UrlbarView.jsm",
});
XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
/**
* Represents the urlbar <textbox>.
* Also forwards important textbox properties and methods.
*/
class UrlbarInput {
/**
* @param {object} options
* The initial options for UrlbarInput.
* @param {object} options.textbox
* The <textbox> element.
* @param {object} options.panel
* The <panel> element.
* @param {UrlbarController} [options.controller]
* Optional fake controller to override the built-in UrlbarController.
* Intended for use in unit tests only.
*/
constructor(options = {}) {
this.textbox = options.textbox;
this.textbox.clickSelectsAll = UrlbarPrefs.get("clickSelectsAll");
this.panel = options.panel;
this.window = this.textbox.ownerGlobal;
this.document = this.window.document;
this.controller = options.controller || new UrlbarController({
browserWindow: this.window,
});
this.controller.setInput(this);
this.view = new UrlbarView(this);
this.valueIsTyped = false;
this.userInitiatedFocus = false;
this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window);
this.lastQueryContextPromise = Promise.resolve();
this._actionOverrideKeyCount = 0;
this._resultForCurrentValue = null;
this._suppressStartQuery = false;
this._untrimmedValue = "";
// Forward textbox methods and properties.
const METHODS = ["addEventListener", "removeEventListener",
"setAttribute", "hasAttribute", "removeAttribute", "getAttribute",
"select"];
const READ_ONLY_PROPERTIES = ["inputField", "editor"];
const READ_WRITE_PROPERTIES = ["placeholder", "readOnly",
"selectionStart", "selectionEnd"];
for (let method of METHODS) {
this[method] = (...args) => {
return this.textbox[method](...args);
};
}
for (let property of READ_ONLY_PROPERTIES) {
Object.defineProperty(this, property, {
enumerable: true,
get() {
return this.textbox[property];
},
});
}
for (let property of READ_WRITE_PROPERTIES) {
Object.defineProperty(this, property, {
enumerable: true,
get() {
return this.textbox[property];
},
set(val) {
return this.textbox[property] = val;
},
});
}
XPCOMUtils.defineLazyGetter(this, "valueFormatter", () => {
return new UrlbarValueFormatter(this);
});
// The event bufferer handles some events, queues them up, and calls back
// our handleEvent at the right time.
this.eventBufferer = new UrlbarEventBufferer(this);
this.inputField.addEventListener("blur", this.eventBufferer);
this.inputField.addEventListener("keydown", this.eventBufferer);
const inputFieldEvents = [
"focus", "input", "keyup", "mouseover", "paste", "scrollend", "select",
"overflow", "underflow",
];
for (let name of inputFieldEvents) {
this.inputField.addEventListener(name, this);
}
this.addEventListener("mousedown", this);
this.view.panel.addEventListener("popupshowing", this);
this.view.panel.addEventListener("popuphidden", this);
this.inputField.controllers.insertControllerAt(0, new CopyCutController(this));
this._initPasteAndGo();
}
/**
* Shortens the given value, usually by removing http:// and trailing slashes,
* such that calling nsIURIFixup::createFixupURI with the result will produce
* the same URI.
*
* @param {string} val
* The string to be trimmed if it appears to be URI
* @returns {string}
* The trimmed string
*/
trimValue(val) {
return UrlbarPrefs.get("trimURLs") ? this.window.trimURL(val) : val;
}
/**
* Applies styling to the text in the urlbar input, depending on the text.
*/
formatValue() {
this.valueFormatter.update();
}
closePopup() {
this.controller.cancelQuery();
this.view.close();
}
focus() {
this.inputField.focus();
}
blur() {
this.inputField.blur();
}
/**
* Converts an internal URI (e.g. a wyciwyg URI) 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 = ReaderMode.getOriginalUrlObjectForDisplay(uri.displaySpec);
if (readerStrippedURI) {
return readerStrippedURI;
}
try {
return Services.uriFixup.createExposableURI(uri);
} catch (ex) {}
return uri;
}
/**
* Passes DOM events for the textbox to the _on_<event type> methods.
* @param {Event} event
* DOM event from the <textbox>.
*/
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 would cause a url or text to be opened.
* XXX the name is currently handleCommand which is compatible with
* urlbarBindings. However, it is no longer called automatically by autocomplete,
* See _on_keydown.
*
* @param {Event} event The event triggering the open.
* @param {string} [openWhere] Where we expect the result to be opened.
* @param {object} [openParams]
* The parameters related to where the result will be opened.
* @param {object} [triggeringPrincipal]
* The principal that the action was triggered from.
*/
handleCommand(event, openWhere, openParams = {}, triggeringPrincipal = null) {
let isMouseEvent = event instanceof this.window.MouseEvent;
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.
let selectedOneOff;
if (this.view.isOpen) {
selectedOneOff = this.view.oneOffSearchButtons.selectedButton;
if (selectedOneOff &&
isMouseEvent &&
event.target != selectedOneOff) {
selectedOneOff = null;
}
// Do the command of the selected one-off if it's not an engine.
if (selectedOneOff && !selectedOneOff.engine) {
selectedOneOff.doCommand();
return;
}
}
// Use the selected result if we have one; this is usually the case
// when the view is open.
let index = this.view.selectedIndex;
if (!selectedOneOff && index != -1) {
this.pickResult(event, index);
return;
}
let url;
if (selectedOneOff) {
// If there's a selected one-off button then load a search using
// the button's engine.
[url, openParams.postData] = UrlbarUtils.getSearchQueryUrl(
selectedOneOff.engine, this._lastSearchString);
this._recordSearch(selectedOneOff.engine, event);
} else {
// Use the current value if we don't have a UrlbarResult e.g. because the
// view is closed.
url = this.value;
openParams.postData = null;
}
if (!url) {
return;
}
let where = openWhere || this._whereToOpen(event);
openParams.allowInheritPrincipal = false;
// TODO: Work out how we get the user selection behavior, probably via passing
// it in, since we don't have the old autocomplete controller to work with.
// BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
// event, this.userSelectionBehavior);
url = this._maybeCanonizeURL(event, url) || url.trim();
try {
new URL(url);
} catch (ex) {
let browser = this.window.gBrowser.selectedBrowser;
let lastLocationChange = browser.lastLocationChange;
UrlbarUtils.getShortcutOrURIAndPostData(url).then(data => {
if (where != "current" ||
browser.lastLocationChange == lastLocationChange) {
openParams.postData = data.postData;
openParams.allowInheritPrincipal = data.mayInheritPrincipal;
this._loadURL(data.url, where, openParams);
}
});
return;
}
this._loadURL(url, where, openParams);
}
handleRevert() {
this.window.gBrowser.userTypedValue = null;
this.window.URLBarSetURI(null, true);
if (this.value && this.focused) {
this.select();
}
}
/**
* Called by the view when a result is picked.
*
* @param {Event} event The event that picked the result.
* @param {resultIndex} resultIndex The index of the result that was picked.
*/
pickResult(event, resultIndex) {
let result = this.view.getResult(resultIndex);
this.setValueFromResult(result);
this.view.close();
// TODO Bug 1500476: Work out how we get the user selection behavior, probably via passing
// it in, since we don't have the old autocomplete controller to work with.
// BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
// event, this.userSelectionBehavior);
this.controller.recordSelectedResult(event, result, resultIndex);
let where = this._whereToOpen(event);
let {url, postData} = UrlbarUtils.getUrlFromResult(result);
let openParams = {
postData,
allowInheritPrincipal: false,
};
if (result.autofill) {
// 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, this._lastSearchString);
if (canonizedUrl) {
this._loadURL(canonizedUrl, where, openParams);
return;
}
}
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
if (this.hasAttribute("actionoverride")) {
where = "current";
break;
}
this.handleRevert();
let prevTab = this.window.gBrowser.selectedTab;
let loadOpts = {
adoptIntoActiveWindow: UrlbarPrefs.get("switchTabs.adoptIntoActiveWindow"),
};
if (this.window.switchToTabHavingURI(Services.io.newURI(url), false, loadOpts) &&
prevTab.isEmpty) {
this.window.gBrowser.removeTab(prevTab);
}
return;
}
case UrlbarUtils.RESULT_TYPE.SEARCH: {
let canonizedUrl = this._maybeCanonizeURL(event,
result.payload.suggestion || result.payload.query);
if (canonizedUrl) {
url = canonizedUrl;
break;
}
const actionDetails = {
isSuggestion: !!result.payload.suggestion,
alias: result.payload.keyword,
};
const engine = Services.search.getEngineByName(result.payload.engine);
this._recordSearch(engine, event, actionDetails);
break;
}
case UrlbarUtils.RESULT_TYPE.OMNIBOX: {
// Give the extension control of handling the command.
ExtensionSearchHandler.handleInputEntered(result.payload.keyword,
result.payload.content,
where);
return;
}
}
if (!url) {
throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
}
this._loadURL(url, where, openParams);
}
/**
* Called by the view when moving through results with the keyboard.
*
* @param {UrlbarResult} result The result that was selected.
*/
setValueFromResult(result) {
if (result.autofill) {
this._setValueFromResultAutofill(result);
} else {
this.value = this._valueFromResultPayload(result);
}
this._resultForCurrentValue = result;
// Also update userTypedValue. See bug 287996.
this.window.gBrowser.userTypedValue = this.value;
// The value setter clobbers the actiontype attribute, so update this after that.
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
this.setAttribute("actiontype", "switchtab");
break;
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
this.setAttribute("actiontype", "extension");
break;
}
}
/**
* Starts a query based on the user input.
*
* @param {number} [options.lastKey]
* The last key the user entered (as a key code).
*/
startQuery({
lastKey = null,
} = {}) {
if (this._suppressStartQuery) {
return;
}
let searchString = this.textValue;
// If the user has deleted text at the end of the input since the last
// query, then we don't want to autofill because doing so would autofill the
// very text the user just deleted.
let enableAutofill =
UrlbarPrefs.get("autoFill") &&
(!this._lastSearchString ||
!this._lastSearchString.startsWith(searchString));
this._lastSearchString = searchString;
// 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 UrlbarQueryContext({
enableAutofill,
isPrivate: this.isPrivate,
lastKey,
maxResults: UrlbarPrefs.get("maxRichResults"),
muxer: "UnifiedComplete",
providers: ["UnifiedComplete"],
searchString,
}));
}
/**
* Sets the input's value, starts a search, and opens the popup.
*
* @param {string} value
* The input's value will be set to this value, and the search will
* use it as its query.
*/
search(value) {
this.window.focusAndSelectUrlBar();
// If the value is a restricted token, append a space.
if (Object.values(UrlbarTokenizer.RESTRICT).includes(value)) {
this.inputField.value = value + " ";
} else {
this.inputField.value = value;
}
// Avoid selecting the text if this method is called twice in a row.
this.selectionStart = -1;
let event = this.document.createEvent("UIEvents");
event.initUIEvent("input", true, false, this.window, 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.textbox.classList.add("hidden-focus");
this.focus();
}
/**
* Remove the hidden focus styles.
* This is used by Activity Stream and about:privatebrowsing for search hand-off.
*/
removeHiddenFocus() {
this.textbox.classList.remove("hidden-focus");
}
// Getters and Setters below.
get focused() {
return this.textbox.getAttribute("focused") == "true";
}
get goButton() {
return this.document.getAnonymousElementByAttribute(this.textbox, "anonid",
"urlbar-go-button");
}
get textValue() {
return this.inputField.value;
}
get value() {
return this._untrimmedValue;
}
set value(val) {
this._untrimmedValue = val;
let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(val);
if (originalUrl) {
val = originalUrl.displaySpec;
}
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;
}
// Private methods below.
_setValueFromResultAutofill(result) {
this.value = result.autofill.value;
this.selectionStart = result.autofill.selectionStart;
this.selectionEnd = result.autofill.selectionEnd;
}
_valueFromResultPayload(result) {
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.KEYWORD:
return result.payload.input;
case UrlbarUtils.RESULT_TYPE.SEARCH:
return (result.payload.keyword ? result.payload.keyword + " " : "") +
(result.payload.suggestion || result.payload.query);
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
return result.payload.content;
}
try {
let uri = Services.io.newURI(result.payload.url);
if (uri) {
return this.window.losslessDecodeURI(uri);
}
} catch (ex) {}
return "";
}
_updateTextOverflow() {
if (!this._overflowing) {
this.removeAttribute("textoverflow");
return;
}
this.window.promiseDocumentFlushed(() => {
// Check overflow again to ensure it didn't change in the meantime.
let input = this.inputField;
if (input && this._overflowing) {
let side = input.scrollLeft &&
input.scrollLeft == input.scrollLeftMax ? "start" : "end";
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.value);
}
}
_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.textValue.replace(selectedVal, "");
if (remainder != "" && remainder[0] != "/") {
return selectedVal;
}
}
let uri;
if (this.getAttribute("pageproxystate") == "valid") {
uri = this.window.gBrowser.currentURI;
} else {
// We're dealing with an autocompleted value.
if (!this._resultForCurrentValue) {
throw new Error("UrlbarInput: Should have a UrlbarResult since " +
"pageproxystate != 'valid' and valueIsTyped == false");
}
let resultURL = this._resultForCurrentValue.payload.url;
if (!resultURL) {
return selectedVal;
}
try {
uri = Services.uriFixup.createFixupURI(resultURL, Services.uriFixup.FIXUP_FLAG_NONE);
} catch (e) {}
if (!uri) {
return selectedVal;
}
}
uri = this.makeURIReadable(uri);
// If the entire URL is selected, just use the actual loaded URI,
// unless we want a decoded URI, or it's a data: or javascript: URI,
// since those are hard to read when encoded.
if (this.textValue == selectedVal &&
!uri.schemeIs("javascript") &&
!uri.schemeIs("data") &&
!UrlbarPrefs.get("decodeURLsOnCopy")) {
return uri.displaySpec;
}
// Just the beginning of the URL is selected, or we want a decoded
// url. First check for a trimmed value.
let spec = uri.displaySpec;
let trimmedSpec = this.trimValue(spec);
if (spec != trimmedSpec) {
// Prepend the portion that trimValue removed from the beginning.
// This assumes trimValue will only truncate the URL at
// the beginning or end (or both).
let trimmedSegments = spec.split(trimmedSpec);
selectedVal = trimmedSegments[0] + selectedVal;
}
return selectedVal;
}
_toggleActionOverride(event) {
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.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 {alias} searchActionDetails.alias
* True if this query was initiated via a search alias.
*/
_recordSearch(engine, event, searchActionDetails = {}) {
const isOneOff = this.view.oneOffSearchButtons.maybeRecordTelemetry(event);
// Infer the type of the event which triggered the search.
let eventType = "unknown";
if (event instanceof KeyboardEvent) {
eventType = "key";
} else if (event instanceof MouseEvent) {
eventType = "mouse";
}
// Augment the search action details object.
let details = searchActionDetails;
details.isOneOff = isOneOff;
details.type = eventType;
this.window.BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
}
/**
* 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 (!(event instanceof KeyboardEvent) ||
!event.ctrlKey ||
!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;
}
value = "http://www." + value;
this.value = value;
return value;
}
/**
* Loads the url in the appropriate place.
*
* @param {string} url
* The URL to open.
* @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]
* If the principal may be inherited
*/
_loadURL(url, openUILinkWhere, params) {
let browser = this.window.gBrowser.selectedBrowser;
// TODO: These should probably be set by the input field.
// this.value = url;
// browser.userTypedValue = url;
if (this.window.gInitialPages.includes(url)) {
browser.initialPageLoadedFromUserAction = url;
}
try {
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.
Cu.reportError(ex);
}
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;
}
// Focus the content area before triggering loads, since if the load
// occurs in a new tab, we want focus to be restored to the content
// area when the current tab is re-selected.
browser.focus();
if (openUILinkWhere != "current") {
this.handleRevert();
}
try {
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();
}
}
// TODO This should probably be handed via input.
// Ensure the start of the URL is visible for usability reasons.
// this.selectionStart = this.selectionEnd = 0;
this.closePopup();
}
/**
* 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 isMouseEvent = event instanceof MouseEvent;
let reuseEmpty = !isMouseEvent;
let where = undefined;
if (!isMouseEvent && event && event.altKey) {
// 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 (!isMouseEvent && event && event.ctrlKey &&
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 (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;
}
_initPasteAndGo() {
let inputBox = this.document.getAnonymousElementByAttribute(
this.textbox, "anonid", "moz-input-box");
// Force the Custom Element to upgrade until Bug 1470242 handles this:
this.window.customElements.upgrade(inputBox);
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");
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.handleCommand();
this._suppressStartQuery = false;
});
contextMenu.addEventListener("popupshowing", () => {
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);
}
// Event handlers below.
_on_blur(event) {
this.formatValue();
this.closePopup();
}
_on_focus(event) {
this._updateUrlTooltip();
this.formatValue();
}
_on_mouseover(event) {
this._updateUrlTooltip();
}
_on_mousedown(event) {
if (event.originalTarget == this.inputField &&
event.button == 0 &&
event.detail == 2 &&
UrlbarPrefs.get("doubleClickSelectsAll")) {
this.editor.selectAll();
event.preventDefault();
return;
}
if (event.originalTarget.classList.contains("urlbar-history-dropmarker") &&
event.button == 0) {
if (this.view.isOpen) {
this.view.close();
} else {
this.startQuery();
}
}
}
_on_input() {
let value = this.textValue;
this.valueIsTyped = true;
this._untrimmedValue = value;
this.window.gBrowser.userTypedValue = value;
if (value) {
this.setAttribute("usertyping", "true");
} else {
this.removeAttribute("usertyping");
}
this.removeAttribute("actiontype");
// XXX Fill in lastKey, and add anything else we need.
this.startQuery({
lastKey: null,
});
}
_on_select(event) {
if (!Services.clipboard.supportsSelectionClipboard()) {
return;
}
if (!this.window.windowUtils.isHandlingUserInput) {
return;
}
let val = this._getSelectedValueForClipboard();
if (!val) {
return;
}
ClipboardHelper.copyStringToClipboard(val, Services.clipboard.kSelectionClipboard);
}
_on_overflow(event) {
const targetIsPlaceholder =
!event.originalTarget.classList.contains("anonymous-div");
// 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.classList.contains("anonymous-div");
// 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.inputField.selectionStart);
// If there is already non-whitespace content in the URL bar
// preceding the pasted content, it's not necessary to check
// protocols used by the pasted content:
if (oldStart.trim()) {
return;
}
let oldEnd = oldValue.substring(this.inputField.selectionEnd);
let pasteData = UrlbarUtils.stripUnsafeProtocolOnPaste(originalPasteData);
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;
// Fix up cursor/selection:
let newCursorPos = oldStart.length + pasteData.length;
this.inputField.selectionStart = newCursorPos;
this.inputField.selectionEnd = newCursorPos;
}
}
_on_scrollend(event) {
this._updateTextOverflow();
}
_on_TabSelect(event) {
this.controller.tabContextChanged();
}
_on_keydown(event) {
this.controller.handleKeyNavigation(event);
this._toggleActionOverride(event);
}
_on_keyup(event) {
this._toggleActionOverride(event);
}
_on_popupshowing() {
this.setAttribute("open", "true");
}
_on_popuphidden() {
this.removeAttribute("open");
}
}
/**
* 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.selectionStart = urlbar.selectionEnd = start;
let event = urlbar.document.createEvent("UIEvents");
event.initUIEvent("input", true, false, urlbar.window, 0);
urlbar.inputField.dispatchEvent(event);
}
ClipboardHelper.copyString(val);
}
/**
* @param {string} command
* @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
* @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() {}
}