Files
tubestation/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs
Drew Willcoxon 212d13a054 Bug 1803873 - Support row buttons in all row types and make changes to tip rows. r=dao
This makes a couple of large changes:

(1) "Generic" buttons (the ones added by `UrlbarView.#addRowButton()`) are now
supported in all row types. The help button that's currently included in some
types of rows when `result.payload.helpUrl` is defined is now supported for all
row types, and two additional button types are now supported too: block buttons
and labeled buttons. A row will get a block button if its
`result.payload.isBlockable` is defined. It will get a labeled button if
`result.payload.buttons` is defined and non-empty. A button can include a `url`
property that is then added as an attribute on the button's element, and
`UrlbarInput.pickResult()` will use this attribute to load the URL when the
button is picked.

(2) The reason I added labeled buttons is because it lets us support tip buttons
without much more effort, which then lets us get rid of the special row type
used for tips. With this patch, tips are now standard rows that use generic
buttons.

This approach should be compatible with the result menu, when we switch over to
it, because we can include the help and block commands in the menu when
`helpUrl` and `isBlockable` are defined, instead of creating buttons for them.
Labeled buttons -- the ones used in tips -- would still be created. The result
menu button itself can continue to be a generic button.

It should also be compatible with including the result menu button inside the
row selection. We'll still add buttons to `.urlbarView-row`, separate from
`.urlbarView-row-inner`, so that the buttons can continue to be on the right
side of the row. We can color the background of the row instead of the
row-inner.

As with D163630, my motivation for this change is to support generic buttons in
dynamic result rows so that help and block buttons can be easily added to
weather suggestions. Here too the larger changes of supporting generic labeled
buttons and removing special rows for tips aren't strictly necessary, but I took
the opportunity to rework things.

Finally, this makes a few other changes:

* It includes some of the more minor improvements to selection that I made in
  D163630.

* It removes the help URL code from quick actions since it was decided not to
  show a help button. Currently, the button is hidden in CSS, but now that a
  generic help button is added for dynamic result rows when
  `result.payload.helpUrl` is defined, `helpUrl` needs to be removed from the
  payload to prevent a button from being added.

* I removed the special tip wrapping behavior, where the tip button and help
  button would wrap below the tip's text. Instead, now the text wraps inside
  row-inner and the buttons always remain on the same horizontal as the text. I
  don't think it's worth the extra complication.

Differential Revision: https://phabricator.services.mozilla.com/D163766
2022-12-06 18:43:49 -05:00

292 lines
8.4 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 {
UrlbarProvider,
UrlbarUtils,
} from "resource:///modules/UrlbarUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickActionsLoaderDefault:
"resource:///modules/QuickActionsLoaderDefault.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
});
// These prefs are relative to the `browser.urlbar` branch.
const ENABLED_PREF = "quickactions.enabled";
const SUGGEST_PREF = "suggest.quickactions";
const MATCH_IN_PHRASE_PREF = "quickactions.matchInPhrase";
const SHOW_IN_ZERO_PREFIX_PREF = "quickactions.showInZeroPrefix";
const DYNAMIC_TYPE_NAME = "quickactions";
// When the urlbar is first focused and no search term has been
// entered we show a limited number of results.
const ACTIONS_SHOWN_FOCUS = 4;
// Default icon shown for actions if no custom one is provided.
const DEFAULT_ICON = "chrome://global/skin/icons/settings.svg";
// The suggestion index of the actions row within the urlbar results.
const SUGGESTED_INDEX = 1;
/**
* A provider that returns a suggested url to the user based on what
* they have currently typed so they can navigate directly.
*/
class ProviderQuickActions extends UrlbarProvider {
constructor() {
super();
lazy.UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME);
Services.tm.idleDispatchToMainThread(() =>
lazy.QuickActionsLoaderDefault.load()
);
}
/**
* Returns the name of this provider.
*
* @returns {string} the name of this provider.
*/
get name() {
return DYNAMIC_TYPE_NAME;
}
/**
* The type of the provider.
*
* @returns {UrlbarUtils.PROVIDER_TYPE}
*/
get type() {
return UrlbarUtils.PROVIDER_TYPE.PROFILE;
}
getPriority(context) {
if (!context.searchString) {
return 1;
}
return 0;
}
/**
* Whether this provider should be invoked for the given context.
* If this method returns false, the providers manager won't start a query
* with this provider, to save on resources.
*
* @param {UrlbarQueryContext} queryContext The query context object
* @returns {boolean} Whether this provider should be invoked for the search.
*/
isActive(queryContext) {
return (
lazy.UrlbarPrefs.get(ENABLED_PREF) &&
((lazy.UrlbarPrefs.get(SUGGEST_PREF) && !queryContext.searchMode) ||
queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.ACTIONS)
);
}
/**
* Starts querying. Extended classes should return a Promise resolved when the
* provider is done searching AND returning results.
*
* @param {UrlbarQueryContext} queryContext The query context object
* @param {Function} addCallback Callback invoked by the provider to add a new
* result. A UrlbarResult should be passed to it.
* @returns {Promise}
*/
async startQuery(queryContext, addCallback) {
let input = queryContext.trimmedSearchString.toLowerCase();
if (!lazy.UrlbarPrefs.get(SHOW_IN_ZERO_PREFIX_PREF) && !input) {
return;
}
let results = [...(this.#prefixes.get(input) ?? [])];
if (lazy.UrlbarPrefs.get(MATCH_IN_PHRASE_PREF)) {
for (let [keyword, key] of this.#keywords) {
if (input.includes(keyword)) {
results.push(key);
}
}
}
// Ensure results are unique.
results = [...new Set(results)];
// Remove invisible actions.
results = results.filter(key => {
const action = this.#actions.get(key);
return !action.isVisible || action.isVisible();
});
if (!results?.length) {
return;
}
// If we are in the Actions searchMode then we want to show all the actions
// but not when we are in the normal url mode on first focus.
if (
results.length > ACTIONS_SHOWN_FOCUS &&
!input &&
!queryContext.searchMode
) {
results.length = ACTIONS_SHOWN_FOCUS;
}
const result = new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.DYNAMIC,
UrlbarUtils.RESULT_SOURCE.ACTIONS,
{
results: results.map(key => ({ key })),
dynamicType: DYNAMIC_TYPE_NAME,
inputLength: input.length,
}
);
result.suggestedIndex = SUGGESTED_INDEX;
addCallback(this, result);
}
getViewTemplate(result) {
return {
children: [
{
name: "buttons",
tag: "div",
children: result.payload.results.map(({ key }, i) => {
let action = this.#actions.get(key);
let inActive = "isActive" in action && !action.isActive();
let row = {
name: `button-${i}`,
tag: "span",
attributes: {
"data-key": key,
"data-input-length": result.payload.inputLength,
class: "urlbarView-quickaction-row",
role: inActive ? "" : "button",
},
children: [
{
name: `icon-${i}`,
tag: "div",
attributes: { class: "urlbarView-favicon" },
children: [
{
name: `image-${i}`,
tag: "img",
attributes: {
class: "urlbarView-favicon-img",
src: action.icon || DEFAULT_ICON,
},
},
],
},
{
name: `label-${i}`,
tag: "span",
attributes: { class: "urlbarView-label" },
},
],
};
if (inActive) {
row.attributes.disabled = "disabled";
}
return row;
}),
},
],
};
}
getViewUpdate(result) {
let viewUpdate = {};
result.payload.results.forEach(({ key }, i) => {
let action = this.#actions.get(key);
viewUpdate[`label-${i}`] = {
l10n: { id: action.label, cacheable: true },
};
});
return viewUpdate;
}
pickResult(result, itemPicked) {
let { key, inputLength } = itemPicked.dataset;
// We clamp the input length to limit the number of keys to
// the number of actions * 10.
inputLength = Math.min(inputLength, 10);
Services.telemetry.keyedScalarAdd(
`quickaction.picked`,
`${key}-${inputLength}`,
1
);
let options = this.#actions.get(itemPicked.dataset.key).onPick() ?? {};
if (options.focusContent) {
itemPicked.ownerGlobal.gBrowser.selectedBrowser.focus();
}
}
/**
* Adds a new QuickAction.
*
* @param {string} key A key to identify this action.
* @param {string} definition An object that describes the action.
*/
addAction(key, definition) {
this.#actions.set(key, definition);
definition.commands.forEach(cmd => this.#keywords.set(cmd, key));
this.#loopOverPrefixes(definition.commands, prefix => {
let result = this.#prefixes.get(prefix);
if (result) {
if (!result.includes(key)) {
result.push(key);
}
} else {
result = [key];
}
this.#prefixes.set(prefix, result);
});
}
/**
* Removes an action.
*
* @param {string} key A key to identify this action.
*/
removeAction(key) {
let definition = this.#actions.get(key);
this.#actions.delete(key);
definition.commands.forEach(cmd => this.#keywords.delete(cmd));
this.#loopOverPrefixes(definition.commands, prefix => {
let result = this.#prefixes.get(prefix);
if (result) {
result = result.filter(val => val != key);
}
this.#prefixes.set(prefix, result);
});
}
// A map from keywords to an action.
#keywords = new Map();
// A map of all prefixes to an array of actions.
#prefixes = new Map();
// The actions that have been added.
#actions = new Map();
#loopOverPrefixes(commands, fun) {
for (const command of commands) {
// Loop over all the prefixes of the word, ie
// "", "w", "wo", "wor", stopping just before the full
// word itself which will be matched by the whole
// phrase matching.
for (let i = 1; i <= command.length; i++) {
let prefix = command.substring(0, command.length - i);
fun(prefix);
}
}
}
}
export var UrlbarProviderQuickActions = new ProviderQuickActions();