Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/browser/components/urlbar/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 124 kB image not shown  

SSL UrlbarView.sys.mjs   Interaktion und
Portierbarkeitunbekannt

 
/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
  ContextualIdentityService:
    "resource://gre/modules/ContextualIdentityService.sys.mjs",
  L10nCache: "resource:///modules/UrlbarUtils.sys.mjs",
  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
  UrlbarProviderGlobalActions:
    "resource:///modules/UrlbarProviderGlobalActions.sys.mjs",
  UrlbarProviderQuickSuggest:
    "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
  UrlbarProviderRecentSearches:
    "resource:///modules/UrlbarProviderRecentSearches.sys.mjs",
  UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs",
  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
  UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
  UrlbarSearchOneOffs: "resource:///modules/UrlbarSearchOneOffs.sys.mjs",
  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
  UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "styleSheetService",
  "@mozilla.org/content/style-sheet-service;1",
  "nsIStyleSheetService"
);

// Query selector for selectable elements in results.
const SELECTABLE_ELEMENT_SELECTOR = "[role=button], [selectable]";
const KEYBOARD_SELECTABLE_ELEMENT_SELECTOR =
  "[role=button]:not([keyboard-inaccessible]), [selectable]";

const ZERO_PREFIX_HISTOGRAM_DWELL_TIME = "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS";

const RESULT_MENU_COMMANDS = {
  DISMISS: "dismiss",
  HELP: "help",
  MANAGE: "manage",
};

const getBoundsWithoutFlushing = element =>
  element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);

// Used to get a unique id to use for row elements, it wraps at 9999, that
// should be plenty for our needs.
let gUniqueIdSerial = 1;
function getUniqueId(prefix) {
  return prefix + (gUniqueIdSerial++ % 9999);
}

/**
 * Receives and displays address bar autocomplete results.
 */
export class UrlbarView {
  // Stale rows are removed on a timer with this timeout.
  static removeStaleRowsTimeout = 400;

  /**
   * @param {UrlbarInput} input
   *   The UrlbarInput instance belonging to this UrlbarView instance.
   */
  constructor(input) {
    this.input = input;
    this.panel = input.panel;
    this.controller = input.controller;
    this.document = this.panel.ownerDocument;
    this.window = this.document.defaultView;

    this.#rows = this.panel.querySelector(".urlbarView-results");
    this.resultMenu = this.panel.querySelector(".urlbarView-result-menu");
    this.#resultMenuCommands = new WeakMap();

    this.#rows.addEventListener("mousedown", this);

    // For the horizontal fade-out effect, set the overflow attribute on result
    // rows when they overflow.
    this.#rows.addEventListener("overflow", this);
    this.#rows.addEventListener("underflow", this);

    this.resultMenu.addEventListener("command", this);
    this.resultMenu.addEventListener("popupshowing", this);

    // `noresults` is used to style the one-offs without their usual top border
    // when no results are present.
    this.panel.setAttribute("noresults", "true");

    this.controller.setView(this);
    this.controller.addQueryListener(this);
    // This is used by autoOpen to avoid flickering results when reopening
    // previously abandoned searches.
    this.queryContextCache = new QueryContextCache(5);

    // We cache l10n strings to avoid Fluent's async lookup.
    this.#l10nCache = new lazy.L10nCache(this.document.l10n);

    for (let viewTemplate of UrlbarView.dynamicViewTemplatesByName.values()) {
      if (viewTemplate.stylesheet) {
        addDynamicStylesheet(this.window, viewTemplate.stylesheet);
      }
    }
  }

  get oneOffSearchButtons() {
    if (!this.#oneOffSearchButtons) {
      this.#oneOffSearchButtons = new lazy.UrlbarSearchOneOffs(this);
      this.#oneOffSearchButtons.addEventListener(
        "SelectedOneOffButtonChanged",
        this
      );
    }
    return this.#oneOffSearchButtons;
  }

  /**
   * Whether the panel is open.
   *
   * @returns {boolean}
   */
  get isOpen() {
    return this.input.hasAttribute("open");
  }

  get allowEmptySelection() {
    let { heuristicResult } = this.#queryContext || {};
    return !heuristicResult || !this.#shouldShowHeuristic(heuristicResult);
  }

  get selectedRowIndex() {
    if (!this.isOpen) {
      return -1;
    }

    let selectedRow = this.#getSelectedRow();

    if (!selectedRow) {
      return -1;
    }

    return selectedRow.result.rowIndex;
  }

  set selectedRowIndex(val) {
    if (!this.isOpen) {
      throw new Error(
        "UrlbarView: Cannot select an item if the view isn't open."
      );
    }

    if (val < 0) {
      this.#selectElement(null);
      return;
    }

    let items = Array.from(this.#rows.children).filter(r =>
      this.#isElementVisible(r)
    );
    if (val >= items.length) {
      throw new Error(`UrlbarView: Index ${val} is out of bounds.`);
    }

    // Select the first selectable element inside the row. If it doesn't
    // contain a selectable element, clear the selection.
    let row = items[val];
    let element = this.#getNextSelectableElement(row);
    if (this.#getRowFromElement(element) != row) {
      element = null;
    }

    this.#selectElement(element);
  }

  get selectedElementIndex() {
    if (!this.isOpen || !this.#selectedElement) {
      return -1;
    }

    return this.#selectedElement.elementIndex;
  }

  /**
   * @returns {UrlbarResult}
   *   The currently selected result.
   */
  get selectedResult() {
    if (!this.isOpen) {
      return null;
    }

    return this.#getSelectedRow()?.result;
  }

  /**
   * @returns {Element}
   *   The currently selected element.
   */
  get selectedElement() {
    if (!this.isOpen) {
      return null;
    }

    return this.#selectedElement;
  }

  /**
   * @returns {boolean}
   *   Whether the SPACE key should activate the selected element (if any)
   *   instead of adding to the input value.
   */
  shouldSpaceActivateSelectedElement() {
    // We want SPACE to activate buttons only.
    if (this.selectedElement?.getAttribute("role") != "button") {
      return false;
    }
    // Make sure the input field is empty, otherwise the user might want to add
    // a space to the current search string. As it stands, selecting a button
    // should always clear the input field, so this is just an extra safeguard.
    if (this.input.value) {
      return false;
    }
    return true;
  }

  /**
   * Clears selection, regardless of view status.
   */
  clearSelection() {
    this.#selectElement(null, { updateInput: false });
  }

  /**
   * @returns {number}
   *   The number of visible results in the view.  Note that this may be larger
   *   than the number of results in the current query context since the view
   *   may be showing stale results.
   */
  get visibleRowCount() {
    let sum = 0;
    for (let row of this.#rows.children) {
      sum += Number(this.#isElementVisible(row));
    }
    return sum;
  }

  /**
   * Returns the result of the row containing the given element, or the result
   * of the element if it itself is a row.
   *
   * @param {Element} element
   *   An element in the view.
   * @returns {UrlbarResult}
   *   The result of the element's row.
   */
  getResultFromElement(element) {
    return element?.classList.contains("urlbarView-result-menuitem")
      ? this.#resultMenuResult
      : this.#getRowFromElement(element)?.result;
  }

  /**
   * @param {number} index
   *   The index from which to fetch the result.
   * @returns {UrlbarResult}
   *   The result at `index`. Null if the view is closed or if there are no
   *   results.
   */
  getResultAtIndex(index) {
    if (
      !this.isOpen ||
      !this.#rows.children.length ||
      index >= this.#rows.children.length
    ) {
      return null;
    }

    return this.#rows.children[index].result;
  }

  /**
   * @param {UrlbarResult} result A result.
   * @returns {boolean} True if the given result is selected.
   */
  resultIsSelected(result) {
    if (this.selectedRowIndex < 0) {
      return false;
    }

    return result.rowIndex == this.selectedRowIndex;
  }

  /**
   * Moves the view selection forward or backward.
   *
   * @param {number} amount
   *   The number of steps to move.
   * @param {object} options Options object
   * @param {boolean} [options.reverse]
   *   Set to true to select the previous item. By default the next item
   *   will be selected.
   * @param {boolean} [options.userPressedTab]
   *   Set to true if the user pressed Tab to select a result. Default false.
   */
  selectBy(amount, { reverse = false, userPressedTab = false } = {}) {
    if (!this.isOpen) {
      throw new Error(
        "UrlbarView: Cannot select an item if the view isn't open."
      );
    }

    // Freeze results as the user is interacting with them, unless we are
    // deferring events while waiting for critical results.
    if (!this.input.eventBufferer.isDeferringEvents) {
      this.controller.cancelQuery();
    }

    if (!userPressedTab) {
      let { selectedRowIndex } = this;
      let end = this.visibleRowCount - 1;
      if (selectedRowIndex == -1) {
        this.selectedRowIndex = reverse ? end : 0;
        return;
      }
      let endReached = selectedRowIndex == (reverse ? 0 : end);
      if (endReached) {
        if (this.allowEmptySelection) {
          this.#selectElement(null);
        } else {
          this.selectedRowIndex = reverse ? end : 0;
        }
        return;
      }

      let index = Math.min(end, selectedRowIndex + amount * (reverse ? -1 : 1));
      // When navigating with arrow keys we skip rows that contain
      // global actions.
      if (
        this.#rows.children[index]?.result.providerName ==
          lazy.UrlbarProviderGlobalActions.name &&
        this.#rows.children.length > 2
      ) {
        index = index + (reverse ? -1 : 1);
      }
      this.selectedRowIndex = Math.max(0, index);
      return;
    }

    // Tab key handling below.

    // Do not set aria-activedescendant if the user is moving to a
    // tab-to-search result with the Tab key. If
    // accessibility.tabToSearch.announceResults is set, the tab-to-search
    // result was announced to the user as they typed. We don't set
    // aria-activedescendant so the user doesn't think they have to press
    // Enter to enter search mode. See bug 1647929.
    const isSkippableTabToSearchAnnounce = selectedElt => {
      let result = this.getResultFromElement(selectedElt);
      let skipAnnouncement =
        result?.providerName == "TabToSearch" &&
        !this.#announceTabToSearchOnSelection &&
        lazy.UrlbarPrefs.get("accessibility.tabToSearch.announceResults");
      if (skipAnnouncement) {
        // Once we skip setting aria-activedescendant once, we should not skip
        // it again if the user returns to that result.
        this.#announceTabToSearchOnSelection = true;
      }
      return skipAnnouncement;
    };

    let selectedElement = this.#selectedElement;

    // We cache the first and last rows since they will not change while
    // selectBy is running.
    let firstSelectableElement = this.getFirstSelectableElement();
    // getLastSelectableElement will not return an element that is over
    // maxResults and thus may be hidden and not selectable.
    let lastSelectableElement = this.getLastSelectableElement();

    if (!selectedElement) {
      selectedElement = reverse
        ? lastSelectableElement
        : firstSelectableElement;
      this.#selectElement(selectedElement, {
        setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
      });
      return;
    }
    let endReached = reverse
      ? selectedElement == firstSelectableElement
      : selectedElement == lastSelectableElement;
    if (endReached) {
      if (this.allowEmptySelection) {
        selectedElement = null;
      } else {
        selectedElement = reverse
          ? lastSelectableElement
          : firstSelectableElement;
      }
      this.#selectElement(selectedElement, {
        setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
      });
      return;
    }

    while (amount-- > 0) {
      let next = reverse
        ? this.#getPreviousSelectableElement(selectedElement)
        : this.#getNextSelectableElement(selectedElement);
      if (!next) {
        break;
      }
      selectedElement = next;
    }
    this.#selectElement(selectedElement, {
      setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
    });
  }

  async acknowledgeFeedback(result) {
    let row = this.#rows.children[result.rowIndex];
    if (!row) {
      return;
    }

    let l10n = { id: "firefox-suggest-feedback-acknowledgment" };
    await this.#l10nCache.ensure(l10n);
    if (row.result != result) {
      return;
    }

    let { value } = this.#l10nCache.get(l10n);
    row.setAttribute("feedback-acknowledgment", value);
    this.window.A11yUtils.announce({
      raw: value,
      source: row._content.closest("[role=option]"),
    });
  }

  /**
   * Replaces the given result's row with a dismissal-acknowledgment tip.
   *
   * @param {UrlbarResult} result
   *   The result that was dismissed.
   * @param {object} titleL10n
   *   The localization object shown as dismissed feedback.
   */
  #acknowledgeDismissal(result, titleL10n) {
    let row = this.#rows.children[result.rowIndex];
    if (!row || row.result != result) {
      return;
    }

    // The row is no longer selectable. It's necessary to clear the selection
    // before replacing the row because replacement will likely create a new
    // `urlbarView-row-inner`, which will interfere with the ability of
    // `#selectElement()` to clear the old selection after replacement, below.
    let isSelected = this.#getSelectedRow() == row;
    if (isSelected) {
      this.#selectElement(null, { updateInput: false });
    }
    this.#setRowSelectable(row, false);

    // Replace the row with a dismissal acknowledgment tip.
    let tip = Object.assign(
      new lazy.UrlbarResult(
        lazy.UrlbarUtils.RESULT_TYPE.TIP,
        lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        {
          type: "dismissalAcknowledgment",
          titleL10n,
          buttons: [{ l10n: { id: "urlbar-search-tips-confirm-short" } }],
          icon: "chrome://branding/content/icon32.png",
        }
      ),
      { rowLabel: this.#rowLabel(row) }
    );
    this.#updateRow(row, tip);
    this.#updateIndices();

    // If the row was selected, move the selection to the tip button.
    if (isSelected) {
      this.#selectElement(this.#getNextSelectableElement(row), {
        updateInput: false,
      });
    }
  }

  removeAccessibleFocus() {
    this.#setAccessibleFocus(null);
  }

  clear() {
    this.#rows.textContent = "";
    this.panel.setAttribute("noresults", "true");
    this.clearSelection();
    this.visibleResults = [];
  }

  /**
   * Hide the popup that shows the Urlbar results. The popup is still
   * considered "open", this will not trigger abandonment telemetry
   * but will not be shown to the user.
   */
  hideTemporarily() {
    this.panel.toggleAttribute("hide-temporarily", true);
  }

  /**
   * Show the Urlbar results popup after being hidden by
   * `hideTemporarily`
   */
  restoreVisibility() {
    this.panel.toggleAttribute("hide-temporarily", false);
  }

  /**
   * Closes the view, cancelling the query if necessary.
   *
   * @param {object} options Options object
   * @param {boolean} [options.elementPicked]
   *   True if the view is being closed because a result was picked.
   * @param {boolean} [options.showFocusBorder]
   *   True if the Urlbar focus border should be shown after the view is closed.
   */
  close({ elementPicked = false, showFocusBorder = true } = {}) {
    this.controller.cancelQuery();
    // We do not show the focus border when an element is picked because we'd
    // flash it just before the input is blurred. The focus border is removed
    // in UrlbarInput._on_blur.
    if (!elementPicked && showFocusBorder) {
      this.input.removeAttribute("suppress-focus-border");
    }

    if (!this.isOpen) {
      return;
    }

    this.#inputWidthOnLastClose = getBoundsWithoutFlushing(
      this.input.textbox
    ).width;

    // We exit search mode preview on close since the result previewing it is
    // implicitly unselected.
    if (this.input.searchMode?.isPreview) {
      this.input.searchMode = null;
      this.window.gBrowser.userTypedValue = null;
    }

    this.resultMenu.hidePopup();
    this.removeAccessibleFocus();
    this.input.inputField.setAttribute("aria-expanded", "false");
    this.#openPanelInstance = null;
    this.#previousTabToSearchEngine = null;

    this.input.removeAttribute("open");
    this.input.endLayoutExtend();

    // Search Tips can open the view without the Urlbar being focused. If the
    // tip is ignored (e.g. the page content is clicked or the window loses
    // focus) we should discard the telemetry event created when the view was
    // opened.
    if (!this.input.focused && !elementPicked) {
      this.controller.engagementEvent.discard();
      this.controller.engagementEvent.record(null, {});
    }

    this.window.removeEventListener("resize", this);
    this.window.removeEventListener("blur", this);

    this.controller.notify(this.controller.NOTIFICATIONS.VIEW_CLOSE);

    // Revoke icon blob URLs that were created while the view was open.
    if (this.#blobUrlsByResultUrl) {
      for (let blobUrl of this.#blobUrlsByResultUrl.values()) {
        URL.revokeObjectURL(blobUrl);
      }
      this.#blobUrlsByResultUrl.clear();
    }

    if (this.#isShowingZeroPrefix) {
      if (elementPicked) {
        Glean.urlbarZeroprefix.engagement.add(1);
      } else {
        Glean.urlbarZeroprefix.abandonment.add(1);
      }
      this.#setIsShowingZeroPrefix(false);
    }
  }

  /**
   * This can be used to open the view automatically as a consequence of
   * specific user actions. For Top Sites searches (without a search string)
   * the view is opened only for mouse or keyboard interactions.
   * If the user abandoned a search (there is a search string) the view is
   * reopened, and we try to use cached results to reduce flickering, then a new
   * query is started to refresh results.
   *
   * @param {object} options Options object
   * @param {Event} options.event The event associated with the call to autoOpen.
   * @param {boolean} [options.suppressFocusBorder] If true, we hide the focus border
   *        when the panel is opened. This is true by default to avoid flashing
   *        the border when the unfocused address bar is clicked.
   * @returns {boolean} Whether the view was opened.
   */
  autoOpen({ event, suppressFocusBorder = true }) {
    if (this.#pickSearchTipIfPresent(event)) {
      return false;
    }

    if (!event) {
      return false;
    }

    let queryOptions = { event };
    if (
      !this.input.value ||
      this.input.getAttribute("pageproxystate") == "valid"
    ) {
      if (!this.isOpen && ["mousedown", "command"].includes(event.type)) {
        // Try to reuse the cached top-sites context. If it's not cached, then
        // there will be a gap of time between when the input is focused and
        // when the view opens that can be perceived as flicker.
        if (!this.input.searchMode && this.queryContextCache.topSitesContext) {
          this.onQueryResults(this.queryContextCache.topSitesContext);
        }
        this.input.startQuery(queryOptions);
        if (suppressFocusBorder) {
          this.input.toggleAttribute("suppress-focus-border", true);
        }
        return true;
      }
      return false;
    }

    // Reopen abandoned searches only if the input is focused.
    if (!this.input.focused) {
      return false;
    }

    // Tab switch is the only case where we requery if the view is open, because
    // switching tabs doesn't necessarily close the view.
    if (this.isOpen && event.type != "tabswitch") {
      return false;
    }

    // We can reuse the current rows as they are if the input value and width
    // haven't changed since the view was closed. The width check is related to
    // row overflow: If we reuse the current rows, overflow and underflow events
    // won't fire even if the view's width has changed and there are rows that
    // do actually overflow or underflow. That means previously overflowed rows
    // may unnecessarily show the overflow gradient, for example.
    if (
      this.#rows.firstElementChild &&
      this.#queryContext.searchString == this.input.value &&
      this.#inputWidthOnLastClose ==
        getBoundsWithoutFlushing(this.input.textbox).width
    ) {
      // We can reuse the current rows.
      queryOptions.allowAutofill = this.#queryContext.allowAutofill;
    } else {
      // To reduce flickering, try to reuse a cached UrlbarQueryContext. The
      // overflow problem is addressed in this case because `onQueryResults()`
      // starts the regular view-update process, during which the overflow state
      // is reset on all rows.
      let cachedQueryContext = this.queryContextCache.get(this.input.value);
      if (cachedQueryContext) {
        this.onQueryResults(cachedQueryContext);
      }
    }

    // Disable autofill when search terms persist, as users are likely refining
    // their search rather than navigating to a website matching the search
    // term. If they do want to navigate directly, users can modify their
    // search, which resets persistence and re-enables autofill.
    let state = this.input.getBrowserState(
      this.window.gBrowser.selectedBrowser
    );
    if (state.persist?.shouldPersist) {
      queryOptions.allowAutofill = false;
    }

    this.controller.engagementEvent.discard();
    queryOptions.searchString = this.input.value;
    queryOptions.autofillIgnoresSelection = true;
    queryOptions.event.interactionType = "returned";

    // A search tip can be cached in results if it was shown but ignored
    // by the user. Don't open the panel if a search tip is present or it
    // will cause a flicker since it'll be quickly overwritten (Bug 1812261).
    if (
      this.#queryContext?.results?.length &&
      this.#queryContext.results[0].type != lazy.UrlbarUtils.RESULT_TYPE.TIP
    ) {
      this.#openPanel();
    }

    // If we had cached results, this will just refresh them, avoiding results
    // flicker, otherwise there may be some noise.
    this.input.startQuery(queryOptions);
    if (suppressFocusBorder) {
      this.input.toggleAttribute("suppress-focus-border", true);
    }
    return true;
  }

  // UrlbarController listener methods.
  onQueryStarted(queryContext) {
    this.#queryWasCancelled = false;
    this.#queryUpdatedResults = false;
    this.#openPanelInstance = null;
    if (!queryContext.searchString) {
      this.#previousTabToSearchEngine = null;
    }
    this.#startRemoveStaleRowsTimer();

    // Cache l10n strings so they're available when we update the view as
    // results arrive. This is a no-op for strings that are already cached.
    // `#cacheL10nStrings` is async but we don't await it because doing so would
    // require view updates to be async. Instead we just opportunistically cache
    // and if there's a cache miss we fall back to `l10n.setAttributes`.
    this.#cacheL10nStrings();
  }

  onQueryCancelled() {
    this.#queryWasCancelled = true;
    this.#cancelRemoveStaleRowsTimer();
  }

  onQueryFinished(queryContext) {
    this.#cancelRemoveStaleRowsTimer();
    if (this.#queryWasCancelled) {
      return;
    }

    // At this point the query finished successfully. If it returned some
    // results, remove stale rows. Otherwise remove all rows.
    if (this.#queryUpdatedResults) {
      this.#removeStaleRows();
    } else {
      this.clear();
    }

    // Now that the view has finished updating for this query, call
    // `#setIsShowingZeroPrefix()`.
    this.#setIsShowingZeroPrefix(!queryContext.searchString);

    // If the query returned results, we're done.
    if (this.#queryUpdatedResults) {
      return;
    }

    // If search mode isn't active, close the view.
    if (!this.input.searchMode) {
      this.close();
      return;
    }

    // Search mode is active.  If the one-offs should be shown, make sure they
    // are enabled and show the view.
    let openPanelInstance = (this.#openPanelInstance = {});
    this.oneOffSearchButtons.willHide().then(willHide => {
      if (!willHide && openPanelInstance == this.#openPanelInstance) {
        this.oneOffSearchButtons.enable(true);
        this.#openPanel();
      }
    });
  }

  onQueryResults(queryContext) {
    this.queryContextCache.put(queryContext);
    this.#queryContext = queryContext;

    if (!this.isOpen) {
      this.clear();
    }
    this.#queryUpdatedResults = true;
    this.#updateResults();

    let firstResult = queryContext.results[0];

    if (queryContext.lastResultCount == 0) {
      // Clear the selection when we get a new set of results.
      this.#selectElement(null, {
        updateInput: false,
      });

      // Show the one-off search buttons unless any of the following are true:
      //  * The first result is a search tip
      //  * The search string is empty
      //  * The search string starts with an `@` or a search restriction
      //    character
      this.oneOffSearchButtons.enable(
        (firstResult.providerName != "UrlbarProviderSearchTips" ||
          queryContext.trimmedSearchString) &&
          queryContext.trimmedSearchString[0] != "@" &&
          (queryContext.trimmedSearchString[0] !=
            lazy.UrlbarTokenizer.RESTRICT.SEARCH ||
            queryContext.trimmedSearchString.length != 1)
      );
    }

    if (!this.#selectedElement && !this.oneOffSearchButtons.selectedButton) {
      if (firstResult.heuristic) {
        // Select the heuristic result.  The heuristic may not be the first
        // result added, which is why we do this check here when each result is
        // added and not above.
        if (this.#shouldShowHeuristic(firstResult)) {
          this.#selectElement(this.getFirstSelectableElement(), {
            updateInput: false,
            setAccessibleFocus:
              this.controller._userSelectionBehavior == "arrow",
          });
        } else {
          this.input.setResultForCurrentValue(firstResult);
        }
      } else if (
        firstResult.payload.providesSearchMode &&
        queryContext.trimmedSearchString != "@"
      ) {
        // Filtered keyword offer results can be in the first position but not
        // be heuristic results. We do this so the user can press Tab to select
        // them, resembling tab-to-search. In that case, the input value is
        // still associated with the first result.
        this.input.setResultForCurrentValue(firstResult);
      }
    }

    // Announce tab-to-search results to screen readers as the user types.
    // Check to make sure we don't announce the same engine multiple times in
    // a row.
    let secondResult = queryContext.results[1];
    if (
      secondResult?.providerName == "TabToSearch" &&
      lazy.UrlbarPrefs.get("accessibility.tabToSearch.announceResults") &&
      this.#previousTabToSearchEngine != secondResult.payload.engine
    ) {
      let engine = secondResult.payload.engine;
      this.window.A11yUtils.announce({
        id: secondResult.payload.isGeneralPurposeEngine
          ? "urlbar-result-action-before-tabtosearch-web"
          : "urlbar-result-action-before-tabtosearch-other",
        args: { engine },
      });
      this.#previousTabToSearchEngine = engine;
      // Do not set aria-activedescendant when the user tabs to the result
      // because we already announced it.
      this.#announceTabToSearchOnSelection = false;
    }

    // If we update the selected element, a new unique ID is generated for it.
    // We need to ensure that aria-activedescendant reflects this new ID.
    if (this.#selectedElement && !this.oneOffSearchButtons.selectedButton) {
      let aadID = this.input.inputField.getAttribute("aria-activedescendant");
      if (aadID && !this.document.getElementById(aadID)) {
        this.#setAccessibleFocus(this.#selectedElement);
      }
    }

    this.#openPanel();

    if (firstResult.heuristic) {
      // The heuristic result may be a search alias result, so apply formatting
      // if necessary.  Conversely, the heuristic result of the previous query
      // may have been an alias, so remove formatting if necessary.
      this.input.formatValue();
    }

    if (queryContext.deferUserSelectionProviders.size) {
      // DeferUserSelectionProviders block user selection until the result is
      // shown, so it's the view's duty to remove them.
      // Doing it sooner, like when the results are added by the provider,
      // would not suffice because there's still a delay before those results
      // reach the view.
      queryContext.results.forEach(r => {
        queryContext.deferUserSelectionProviders.delete(r.providerName);
      });
    }
  }

  /**
   * Handles removing a result from the view when it is removed from the query,
   * and attempts to select the new result on the same row.
   *
   * This assumes that the result rows are in index order.
   *
   * @param {number} index The index of the result that has been removed.
   */
  onQueryResultRemoved(index) {
    let rowToRemove = this.#rows.children[index];

    let { result } = rowToRemove;
    if (result.acknowledgeDismissalL10n) {
      // Replace the result's row with a dismissal acknowledgment tip.
      this.#acknowledgeDismissal(result, result.acknowledgeDismissalL10n);
      return;
    }

    let updateSelection = rowToRemove == this.#getSelectedRow();
    rowToRemove.remove();
    this.#updateIndices();

    if (!updateSelection) {
      return;
    }
    // Select the row at the same index, if possible.
    let newSelectionIndex = index;
    if (index >= this.#queryContext.results.length) {
      newSelectionIndex = this.#queryContext.results.length - 1;
    }
    if (newSelectionIndex >= 0) {
      this.selectedRowIndex = newSelectionIndex;
    }
  }

  openResultMenu(result, anchor) {
    this.#resultMenuResult = result;

    if (AppConstants.platform == "macosx") {
      // `openPopup(anchor)` doesn't use a native context menu, which is very
      // noticeable on Mac. Use `openPopup()` with x and y coords instead. See
      // bug 1831760 and bug 1710459.
      let rect = getBoundsWithoutFlushing(anchor);
      rect = this.window.windowUtils.toScreenRectInCSSUnits(
        rect.x,
        rect.y,
        rect.width,
        rect.height
      );
      this.resultMenu.openPopup(null, {
        x: rect.x,
        y: rect.y + rect.height,
      });
    } else {
      this.resultMenu.openPopup(anchor, "bottomright topright");
    }

    anchor.toggleAttribute("open", true);
    let listener = event => {
      if (event.target == this.resultMenu) {
        anchor.removeAttribute("open");
        this.resultMenu.removeEventListener("popuphidden", listener);
      }
    };
    this.resultMenu.addEventListener("popuphidden", listener);
  }

  /**
   * Clears the result menu commands cache, removing the cached commands for all
   * results. This is useful when the commands for one or more results change
   * while the results remain in the view.
   */
  invalidateResultMenuCommands() {
    this.#resultMenuCommands = new WeakMap();
  }

  /**
   * Passes DOM events for the view to the on_<event type> methods.
   *
   * @param {Event} event
   *   DOM event from the <view>.
   */
  handleEvent(event) {
    let methodName = "on_" + event.type;
    if (methodName in this) {
      this[methodName](event);
    } else {
      throw new Error("Unrecognized UrlbarView event: " + event.type);
    }
  }

  static dynamicViewTemplatesByName = new Map();

  /**
   * Registers the view template for a dynamic result type.  A view template is
   * a plain object that describes the DOM subtree for a dynamic result type.
   * When a dynamic result is shown in the urlbar view, its type's view template
   * is used to construct the part of the view that represents the result.
   *
   * The specified view template will be available to the urlbars in all current
   * and future browser windows until it is unregistered.  A given dynamic
   * result type has at most one view template.  If this method is called for a
   * dynamic result type more than once, the view template in the last call
   * overrides those in previous calls.
   *
   * @param {string} name
   *   The view template will be registered for the dynamic result type with
   *   this name.
   * @param {object} viewTemplate
   *   This object describes the DOM subtree for the given dynamic result type.
   *   It should be a tree-like nested structure with each object in the nesting
   *   representing a DOM element to be created.  This tree-like structure is
   *   achieved using the `children` property described below.  Each object in
   *   the structure may include the following properties:
   *
   *   {string} name
   *     The name of the object.  It is required for all objects in the
   *     structure except the root object and serves two important functions:
   *     (1) The element created for the object will automatically have a class
   *         named `urlbarView-dynamic-${dynamicType}-${name}`, where
   *         `dynamicType` is the name of the dynamic result type.  The element
   *         will also automatically have an attribute "name" whose value is
   *         this name.  The class and attribute allow the element to be styled
   *         in CSS.
   *     (2) The name is used when updating the view.  See
   *         UrlbarProvider.getViewUpdate().
   *     Names must be unique within a view template, but they don't need to be
   *     globally unique.  i.e., two different view templates can use the same
   *     names, and other DOM elements can use the same names in their IDs and
   *     classes.  The name also suffixes the dynamic element's ID: an element
   *     with name `data` will get the ID `urlbarView-row-{unique number}-data`.
   *     If there is no name provided for the root element, the root element
   *     will not get an ID.
   *   {string} tag
   *     The tag name of the object.  It is required for all objects in the
   *     structure except the root object and declares the kind of element that
   *     will be created for the object: span, div, img, etc.
   *   {object} [attributes]
   *     An optional mapping from attribute names to values.  For each
   *     name-value pair, an attribute is added to the element created for the
   *     object. The `id` attribute is reserved and cannot be set by the
   *     provider. Element IDs are passed back to the provider in getViewUpdate
   *     if they are needed.
   *   {array} [children]
   *     An optional list of children.  Each item in the array must be an object
   *     as described here.  For each item, a child element as described by the
   *     item is created and added to the element created for the parent object.
   *   {array} [classList]
   *     An optional list of classes.  Each class will be added to the element
   *     created for the object by calling element.classList.add().
   *   {boolean} [overflowable]
   *     If true, the element's overflow status will be tracked in order to
   *     fade it out when needed.
   *   {string} [stylesheet]
   *     An optional stylesheet URL.  This property is valid only on the root
   *     object in the structure.  The stylesheet will be loaded in all browser
   *     windows so that the dynamic result type view may be styled.
   */
  static addDynamicViewTemplate(name, viewTemplate) {
    this.dynamicViewTemplatesByName.set(name, viewTemplate);
    if (viewTemplate.stylesheet) {
      for (let window of lazy.BrowserWindowTracker.orderedWindows) {
        addDynamicStylesheet(window, viewTemplate.stylesheet);
      }
    }
  }

  /**
   * Unregisters the view template for a dynamic result type.
   *
   * @param {string} name
   *   The view template will be unregistered for the dynamic result type with
   *   this name.
   */
  static removeDynamicViewTemplate(name) {
    let viewTemplate = this.dynamicViewTemplatesByName.get(name);
    if (!viewTemplate) {
      return;
    }
    this.dynamicViewTemplatesByName.delete(name);
    if (viewTemplate.stylesheet) {
      for (let window of lazy.BrowserWindowTracker.orderedWindows) {
        removeDynamicStylesheet(window, viewTemplate.stylesheet);
      }
    }
  }

  // Private properties and methods below.
  #announceTabToSearchOnSelection;
  #blobUrlsByResultUrl = null;
  #inputWidthOnLastClose = 0;
  #l10nCache;
  #mousedownSelectedElement;
  #openPanelInstance;
  #oneOffSearchButtons;
  #previousTabToSearchEngine;
  #queryContext;
  #queryUpdatedResults;
  #queryWasCancelled;
  #removeStaleRowsTimer;
  #resultMenuResult;
  #resultMenuCommands;
  #rows;
  #rawSelectedElement;
  #zeroPrefixStopwatchInstance = null;

  /**
   * #rawSelectedElement may be disconnected from the DOM (e.g. it was remove()d)
   * but we want a connected #selectedElement usually. We don't use a WeakRef
   * because it would depend too much on GC timing.
   *
   * @returns {DOMElement} the selected element.
   */
  get #selectedElement() {
    return this.#rawSelectedElement?.isConnected
      ? this.#rawSelectedElement
      : null;
  }

  #createElement(name) {
    return this.document.createElementNS("http://www.w3.org/1999/xhtml", name);
  }

  #openPanel() {
    if (this.isOpen) {
      return;
    }
    this.controller.userSelectionBehavior = "none";

    this.panel.removeAttribute("action-override");

    this.#enableOrDisableRowWrap();

    this.input.inputField.setAttribute("aria-expanded", "true");

    this.input.toggleAttribute("suppress-focus-border", true);
    this.input.toggleAttribute("open", true);
    this.input.startLayoutExtend();

    this.window.addEventListener("resize", this);
    this.window.addEventListener("blur", this);

    this.controller.notify(this.controller.NOTIFICATIONS.VIEW_OPEN);

    if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) {
      this.window.docShell.treeOwner
        .QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIAppWindow)
        .rollupAllPopups();
    }
  }

  #shouldShowHeuristic(result) {
    if (!result?.heuristic) {
      throw new Error("A heuristic result must be given");
    }
    return (
      !lazy.UrlbarPrefs.get("experimental.hideHeuristic") ||
      result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP
    );
  }

  /**
   * Whether a result is a search suggestion.
   *
   * @param {UrlbarResult} result The result to examine.
   * @returns {boolean} Whether the result is a search suggestion.
   */
  #resultIsSearchSuggestion(result) {
    return Boolean(
      result &&
        result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
        result.payload.suggestion
    );
  }

  /**
   * Checks whether the given row index can be update to the result we want
   * to apply. This is used in #updateResults to avoid flickering of results, by
   * reusing existing rows.
   *
   * @param {number} rowIndex Index of the row to examine.
   * @param {UrlbarResult} result The result we'd like to apply.
   * @param {boolean} seenSearchSuggestion Whether the view update has
   *        encountered an existing row with a search suggestion result.
   * @returns {boolean} Whether the row can be updated to this result.
   */
  #rowCanUpdateToResult(rowIndex, result, seenSearchSuggestion) {
    // The heuristic result must always be current, thus it's always compatible.
    // Note that the `updateResults` code, when updating the selection, relies
    // on the fact the heuristic is the first selectable row.
    if (result.heuristic) {
      return true;
    }
    let row = this.#rows.children[rowIndex];
    // Don't replace a suggestedIndex result with a non-suggestedIndex result
    // or vice versa.
    if (result.hasSuggestedIndex != row.result.hasSuggestedIndex) {
      return false;
    }
    // Don't replace a suggestedIndex result with another suggestedIndex
    // result if the suggestedIndex values are different.
    if (
      result.hasSuggestedIndex &&
      result.suggestedIndex != row.result.suggestedIndex
    ) {
      return false;
    }
    // To avoid flickering results while typing, don't try to reuse results from
    // different providers.
    // For example user types "moz", provider A returns results much earlier
    // than provider B, but results from provider B stabilize in the view at the
    // end of the search. Typing the next letter "i" results from the faster
    // provider A would temporarily replace old results from provider B, just
    // to be replaced as soon as provider B returns its results.
    if (result.providerName != row.result.providerName) {
      return false;
    }
    let resultIsSearchSuggestion = this.#resultIsSearchSuggestion(result);
    // If the row is same type, just update it.
    if (
      resultIsSearchSuggestion == this.#resultIsSearchSuggestion(row.result)
    ) {
      return true;
    }
    // If the row has a different type, update it if we are in a compatible
    // index range.
    // In practice we don't want to overwrite a search suggestion with a non
    // search suggestion, but we allow the opposite.
    return resultIsSearchSuggestion && seenSearchSuggestion;
  }

  #updateResults() {
    // TODO: For now this just compares search suggestions to the rest, in the
    // future we should make it support any type of result. Or, even better,
    // results should be grouped, thus we can directly update groups.

    // Discard tentative exposures. This is analogous to marking the
    // hypothetical hidden rows of hidden-exposure results as stale.
    this.controller.engagementEvent.discardTentativeExposures();

    // Walk rows and find an insertion index for results. To avoid flicker, we
    // skip rows until we find one compatible with the result we want to apply.
    // If we couldn't find a compatible range, we'll just update.
    let results = this.#queryContext.results;
    if (results[0]?.heuristic && !this.#shouldShowHeuristic(results[0])) {
      // Exclude the heuristic.
      results = results.slice(1);
    }
    let rowIndex = 0;
    let resultIndex = 0;
    let visibleSpanCount = 0;
    let seenMisplacedResult = false;
    let seenSearchSuggestion = false;

    // Update each row with the next new result until we either encounter a row
    // that can't be updated or run out of new results. At that point, mark
    // remaining rows as stale.
    while (
      rowIndex < this.#rows.children.length &&
      resultIndex < results.length
    ) {
      let row = this.#rows.children[rowIndex];
      if (this.#isElementVisible(row)) {
        visibleSpanCount += lazy.UrlbarUtils.getSpanForResult(row.result);
      }

      if (!seenMisplacedResult) {
        let result = results[resultIndex];
        seenSearchSuggestion =
          seenSearchSuggestion ||
          (!row.result.heuristic && this.#resultIsSearchSuggestion(row.result));
        if (
          this.#rowCanUpdateToResult(rowIndex, result, seenSearchSuggestion)
        ) {
          // We can replace the row's current result with the new one.
          resultIndex++;

          if (result.isHiddenExposure) {
            // Don't increment `rowIndex` because we're not actually updating
            // the row. We'll visit it again in the next iteration.
            this.controller.engagementEvent.addExposure(
              result,
              this.#queryContext
            );
            continue;
          }

          this.#updateRow(row, result);
          rowIndex++;
          continue;
        }

        if (
          (result.hasSuggestedIndex || row.result.hasSuggestedIndex) &&
          !result.isHiddenExposure
        ) {
          seenMisplacedResult = true;
        }
      }

      row.setAttribute("stale", "true");
      rowIndex++;
    }

    // Mark all the remaining rows as stale and update the visible span count.
    // We include stale rows in the count because we should never show more than
    // maxResults spans at one time.  Later we'll remove stale rows and unhide
    // excess non-stale rows.
    for (; rowIndex < this.#rows.children.length; ++rowIndex) {
      let row = this.#rows.children[rowIndex];
      row.setAttribute("stale", "true");
      if (this.#isElementVisible(row)) {
        visibleSpanCount += lazy.UrlbarUtils.getSpanForResult(row.result);
      }
    }

    // Add remaining results, if we have fewer rows than results.
    for (; resultIndex < results.length; ++resultIndex) {
      let result = results[resultIndex];
      if (
        !seenMisplacedResult &&
        result.hasSuggestedIndex &&
        !result.isHiddenExposure
      ) {
        if (result.isSuggestedIndexRelativeToGroup) {
          // We can't know at this point what the right index of a group-
          // relative suggestedIndex result will be. To avoid all all possible
          // flicker, don't make it (and all rows after it) visible until stale
          // rows are removed.
          seenMisplacedResult = true;
        } else {
          // We need to check whether the new suggestedIndex result will end up
          // at its right index if we append it here. The "right" index is the
          // final index the result will occupy once the update is done and all
          // stale rows have been removed. We could use a more flexible
          // definition, but we use this strict one in order to avoid all
          // perceived flicker and movement of suggestedIndex results. Once
          // stale rows are removed, the final number of rows in the view will
          // be the new result count, so we base our arithmetic here on it.
          let finalIndex =
            result.suggestedIndex >= 0
              ? Math.min(results.length - 1, result.suggestedIndex)
              : Math.max(0, results.length + result.suggestedIndex);
          if (this.#rows.children.length != finalIndex) {
            seenMisplacedResult = true;
          }
        }
      }
      let newSpanCount =
        visibleSpanCount +
        lazy.UrlbarUtils.getSpanForResult(result, {
          includeHiddenExposures: true,
        });
      let canBeVisible =
        newSpanCount <= this.#queryContext.maxResults && !seenMisplacedResult;
      if (result.isHiddenExposure) {
        if (canBeVisible) {
          this.controller.engagementEvent.addExposure(
            result,
            this.#queryContext
          );
        } else {
          // Add a tentative exposure: The hypothetical row for this
          // hidden-exposure result can't be visible now, but as long as it were
          // not marked stale in a later update, it would be shown when stale
          // rows are removed.
          this.controller.engagementEvent.addTentativeExposure(
            result,
            this.#queryContext
          );
        }
        continue;
      }
      let row = this.#createRow();
      this.#updateRow(row, result);
      if (canBeVisible) {
        visibleSpanCount = newSpanCount;
      } else {
        // The new row must be hidden at first because the view is already
        // showing maxResults spans, or we encountered a new suggestedIndex
        // result that couldn't be placed in the right spot. We'll show it when
        // stale rows are removed.
        this.#setRowVisibility(row, false);
      }
      this.#rows.appendChild(row);
    }

    this.#updateIndices();
  }

  #createRow() {
    let item = this.#createElement("div");
    item.className = "urlbarView-row";
    item._elements = new Map();
    item._buttons = new Map();

    // A note about row selection. Any element in a row that can be selected
    // will have the `selectable` attribute set on it. For typical rows, the
    // selectable element is not the `.urlbarView-row` itself but rather the
    // `.urlbarView-row-inner` inside it. That's because the `.urlbarView-row`
    // also contains the row's buttons, which should not be selected when the
    // main part of the row -- `.urlbarView-row-inner` -- is selected.
    //
    // Since it's the row itself and not the row-inner that is a child of the
    // `role=listbox` element (the rows container, `this.#rows`), screen readers
    // will not automatically recognize the row-inner as a listbox option. To
    // compensate, we set `role=option` on the row-inner and `role=presentation`
    // on the row itself so that screen readers ignore it.
    item.setAttribute("role", "presentation");

    // These are used to cleanup result specific entities when row contents are
    // cleared to reuse the row for a different result.
    item._sharedAttributes = new Set(
      [...item.attributes].map(v => v.name).concat(["stale", "id"])
    );
    item._sharedClassList = new Set(item.classList);

    return item;
  }

  #createRowContent(item) {
    // The url is the only element that can wrap, thus all the other elements
    // are child of noWrap.
    let noWrap = this.#createElement("span");
    noWrap.className = "urlbarView-no-wrap";
    item._content.appendChild(noWrap);

    let favicon = this.#createElement("img");
    favicon.className = "urlbarView-favicon";
    noWrap.appendChild(favicon);
    item._elements.set("favicon", favicon);

    let typeIcon = this.#createElement("span");
    typeIcon.className = "urlbarView-type-icon";
    noWrap.appendChild(typeIcon);

    let tailPrefix = this.#createElement("span");
    tailPrefix.className = "urlbarView-tail-prefix";
    noWrap.appendChild(tailPrefix);
    item._elements.set("tailPrefix", tailPrefix);
    // tailPrefix holds text only for alignment purposes so it should never be
    // read to screen readers.
    tailPrefix.toggleAttribute("aria-hidden", true);

    let tailPrefixStr = this.#createElement("span");
    tailPrefixStr.className = "urlbarView-tail-prefix-string";
    tailPrefix.appendChild(tailPrefixStr);
    item._elements.set("tailPrefixStr", tailPrefixStr);

    let tailPrefixChar = this.#createElement("span");
    tailPrefixChar.className = "urlbarView-tail-prefix-char";
    tailPrefix.appendChild(tailPrefixChar);
    item._elements.set("tailPrefixChar", tailPrefixChar);

    let title = this.#createElement("span");
    title.classList.add("urlbarView-title", "urlbarView-overflowable");
    noWrap.appendChild(title);
    item._elements.set("title", title);

    let tagsContainer = this.#createElement("span");
    tagsContainer.classList.add("urlbarView-tags", "urlbarView-overflowable");
    noWrap.appendChild(tagsContainer);
    item._elements.set("tagsContainer", tagsContainer);

    let titleSeparator = this.#createElement("span");
    titleSeparator.className = "urlbarView-title-separator";
    noWrap.appendChild(titleSeparator);
    item._elements.set("titleSeparator", titleSeparator);

    let action = this.#createElement("span");
    action.className = "urlbarView-action";
    noWrap.appendChild(action);
    item._elements.set("action", action);

    let url = this.#createElement("span");
    url.className = "urlbarView-url";
    item._content.appendChild(url);
    item._elements.set("url", url);
  }

  /**
   * @param {Element} node
   *   The element to set attributes on.
   * @param {object} attributes
   *   Attribute names to values mapping.  For each name-value pair, an
   *   attribute is set on the element, except for `null` as a value which
   *   signals an attribute should be removed, and `undefined` in which case
   *   the attribute won't be set nor removed. The `id` attribute is reserved
   *   and cannot be set here.
   * @param {UrlbarResult} result
   *   The UrlbarResult displayed to the node. This is optional.
   */
  #setDynamicAttributes(node, attributes, result) {
    if (!attributes) {
      return;
    }
    for (let [name, value] of Object.entries(attributes)) {
      if (name == "id") {
        // IDs are managed externally to ensure they are unique.
        console.error(
          `Not setting id="${value}", as dynamic attributes may not include IDs.`
        );
        continue;
      }
      if (value === undefined) {
        continue;
      }
      if (value === null) {
        node.removeAttribute(name);
      } else if (typeof value == "boolean") {
        node.toggleAttribute(name, value);
      } else if (Blob.isInstance(value) && result) {
        node.setAttribute(name, this.#getBlobUrlForResult(result, value));
      } else {
        node.setAttribute(name, value);
      }
    }
  }

  #createRowContentForDynamicType(item, result) {
    let { dynamicType } = result.payload;
    let provider = lazy.UrlbarProvidersManager.getProvider(result.providerName);
    let viewTemplate =
      provider.getViewTemplate?.(result) ||
      UrlbarView.dynamicViewTemplatesByName.get(dynamicType);
    if (!viewTemplate) {
      console.error(`No viewTemplate found for ${result.providerName}`);
      return;
    }
    let classes = this.#buildViewForDynamicType(
      dynamicType,
      item._content,
      item._elements,
      viewTemplate
    );
    item.toggleAttribute("has-url", classes.has("urlbarView-url"));
    item.toggleAttribute("has-action", classes.has("urlbarView-action"));
    this.#setRowSelectable(item, item._content.hasAttribute("selectable"));
  }

  /**
   * Recursively builds a row's DOM for a dynamic result type.
   *
   * @param {string} type
   *   The name of the dynamic type.
   * @param {Element} parentNode
   *   The element being recursed into. Pass `row._content`
   *   (i.e., the row's `.urlbarView-row-inner`) to start with.
   * @param {Map} elementsByName
   *   The `row._elements` map.
   * @param {object} template
   *   The template object being recursed into. Pass the top-level template
   *   object to start with.
   * @param {Set} classes
   *   The CSS class names of all elements in the row's subtree are recursively
   *   collected in this set. Don't pass anything to start with so that the
   *   default argument, a new Set, is used.
   * @returns {Set}
   *   The `classes` set, which on return will contain the CSS class names of
   *   all elements in the row's subtree.
   */
  #buildViewForDynamicType(
    type,
    parentNode,
    elementsByName,
    template,
    classes = new Set()
  ) {
    // Set attributes on parentNode.
    this.#setDynamicAttributes(parentNode, template.attributes);

    // Add classes to parentNode's classList.
    if (template.classList) {
      parentNode.classList.add(...template.classList);
      for (let c of template.classList) {
        classes.add(c);
      }
    }
    if (template.overflowable) {
      parentNode.classList.add("urlbarView-overflowable");
    }
    if (template.name) {
      parentNode.setAttribute("name", template.name);
      elementsByName.set(template.name, parentNode);
    }

    // Recurse into children.
    for (let childTemplate of template.children || []) {
      let child = this.#createElement(childTemplate.tag);
      child.classList.add(`urlbarView-dynamic-${type}-${childTemplate.name}`);
      parentNode.appendChild(child);
      this.#buildViewForDynamicType(
        type,
        child,
        elementsByName,
        childTemplate,
        classes
      );
    }

    return classes;
  }

  #createRowContentForRichSuggestion(item) {
    item._content.toggleAttribute("selectable", true);

    let favicon = this.#createElement("img");
    favicon.className = "urlbarView-favicon";
    item._content.appendChild(favicon);
    item._elements.set("favicon", favicon);

    let body = this.#createElement("span");
    body.className = "urlbarView-row-body";
    item._content.appendChild(body);

    let top = this.#createElement("div");
    top.className = "urlbarView-row-body-top";
    body.appendChild(top);

    let noWrap = this.#createElement("div");
    noWrap.className = "urlbarView-row-body-top-no-wrap";
    top.appendChild(noWrap);
    item._elements.set("noWrap", noWrap);

    let title = this.#createElement("span");
    title.classList.add("urlbarView-title", "urlbarView-overflowable");
    noWrap.appendChild(title);
    item._elements.set("title", title);

    let titleSeparator = this.#createElement("span");
    titleSeparator.className = "urlbarView-title-separator";
    noWrap.appendChild(titleSeparator);
    item._elements.set("titleSeparator", titleSeparator);

    let action = this.#createElement("span");
    action.className = "urlbarView-action";
    noWrap.appendChild(action);
    item._elements.set("action", action);

    let url = this.#createElement("span");
    url.className = "urlbarView-url";
    top.appendChild(url);
    item._elements.set("url", url);

    let description = this.#createElement("div");
    description.classList.add("urlbarView-row-body-description");
    body.appendChild(description);
    item._elements.set("description", description);

    let bottom = this.#createElement("div");
    bottom.className = "urlbarView-row-body-bottom";
    body.appendChild(bottom);
    item._elements.set("bottom", bottom);
  }

  #addRowButtons(item, result) {
    for (let i = 0; i < result.payload.buttons?.length; i++) {
      this.#addRowButton(item, {
        name: i.toString(),
        ...result.payload.buttons[i],
      });
    }

    // TODO: `buttonText` is intended only for WebExtensions. We should remove
    // it and the WebExtensions urlbar API since we're no longer using it.
    if (result.payload.buttonText) {
      this.#addRowButton(item, {
        name: "tip",
        url: result.payload.buttonUrl,
      });
      item._buttons.get("tip").textContent = result.payload.buttonText;
    }

    if (this.#getResultMenuCommands(result)) {
      this.#addRowButton(item, {
        name: "menu",
        l10n: {
          id: result.showFeedbackMenu
            ? "urlbar-result-menu-button-feedback"
            : "urlbar-result-menu-button",
        },
        attributes: lazy.UrlbarPrefs.get("resultMenu.keyboardAccessible")
          ? null
          : {
              "keyboard-inaccessible": true,
            },
      });
    }
  }

  #addRowButton(item, { name, command, l10n, url, attributes }) {
    let button = this.#createElement("span");
    this.#setDynamicAttributes(button, attributes);
    button.id = `${item.id}-button-${name}`;
    button.classList.add("urlbarView-button", "urlbarView-button-" + name);
    button.setAttribute("role", "button");
    button.dataset.name = name;
    if (l10n) {
      this.#l10nCache.setElementL10n(button, l10n);
    }
    if (command) {
      button.dataset.command = command;
    }
    if (url) {
      button.dataset.url = url;
    }
    item._buttons.set(name, button);
    item.appendChild(button);
  }

  #createSecondaryAction(action, global = false) {
    let actionContainer = this.#createElement("div");
    actionContainer.classList.add("urlbarView-actions-container");

    let button = this.#createElement("span");
    button.classList.add("urlbarView-action-btn");
    if (global) {
      button.classList.add("urlbarView-global-action-btn");
    }
    if (action.classList) {
      button.classList.add(...action.classList);
    }
    button.setAttribute("role", "button");
    if (action.icon) {
      let icon = this.#createElement("img");
      icon.src = action.icon;
      button.appendChild(icon);
    }
    for (let key in action.dataset ?? {}) {
      button.dataset[key] = action.dataset[key];
    }
    button.dataset.action = action.key;
    button.dataset.providerName = action.providerName;

    let label = this.#createElement("span");
    if (action.l10nId) {
      this.#l10nCache.setElementL10n(label, {
        id: action.l10nId,
        args: action.l10nArgs,
      });
    } else {
      this.document.l10n.setAttributes(label, action.label, action.l10nArgs);
    }
    button.appendChild(label);
    actionContainer.appendChild(button);
    return actionContainer;
  }

  // eslint-disable-next-line complexity
  #updateRow(item, result) {
    let oldResult = item.result;
    let oldResultType = item.result?.type;
    let provider = lazy.UrlbarProvidersManager.getProvider(result.providerName);
    item.result = result;
    item.removeAttribute("stale");
    item.id = getUniqueId("urlbarView-row-");

    let needsNewContent =
      oldResultType === undefined ||
      (oldResultType == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) !=
        (result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) ||
      (oldResultType == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC &&
        result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC &&
        oldResult.payload.dynamicType != result.payload.dynamicType) ||
      // Dynamic results that implement getViewTemplate will
      // always need updating.
      provider?.getViewTemplate ||
      oldResult.isRichSuggestion != result.isRichSuggestion ||
      !!this.#getResultMenuCommands(result) != item._buttons.has("menu") ||
      !!oldResult.showFeedbackMenu != !!result.showFeedbackMenu ||
      !lazy.ObjectUtils.deepEqual(
        oldResult.payload.buttons,
        result.payload.buttons
      ) ||
      // Reusing a non-heuristic as a heuristic is risky as it may have DOM
      // nodes/attributes/classes that are normally not present in a heuristic
      // result. This may happen for example when switching from a zero-prefix
      // search not having a heuristic to a search string one.
      result.heuristic != oldResult.heuristic ||
      // Container switch-tab results have a more complex DOM content that is
      // only updated correctly by another switch-tab result.
      (oldResultType == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
        lazy.UrlbarProviderOpenTabs.isContainerUserContextId(
          oldResult.payload.userContextId
        ) &&
        result.type != oldResultType) ||
      result.testForceNewContent;

    if (needsNewContent) {
      while (item.lastChild) {
        item.lastChild.remove();
      }
      item._elements.clear();
      item._buttons.clear();
      item._content = this.#createElement("span");
      item._content.className = "urlbarView-row-inner";
      item.appendChild(item._content);
      // Clear previously set attributes and classes that may refer to a
      // different result type.
      for (const attribute of item.attributes) {
        if (!item._sharedAttributes.has(attribute.name)) {
          item.removeAttribute(attribute.name);
        }
      }
      for (const className of item.classList) {
        if (!item._sharedClassList.has(className)) {
          item.classList.remove(className);
        }
      }
      if (item.result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) {
        this.#createRowContentForDynamicType(item, result);
      } else if (result.isRichSuggestion) {
        this.#createRowContentForRichSuggestion(item, result);
      } else {
        this.#createRowContent(item, result);
      }
      this.#addRowButtons(item, result);
    }
    item._content.id = item.id + "-inner";

    let isFirstChild = item === this.#rows.children[0];
    let secAction = result.payload.action;
    let container = item.querySelector(".urlbarView-actions-container");
    item.toggleAttribute("secondary-action", !!secAction);
    if (secAction && !container) {
      item.appendChild(this.#createSecondaryAction(secAction, isFirstChild));
    } else if (
      secAction &&
      secAction.key != container.firstChild.dataset.action
    ) {
      item.replaceChild(
        this.#createSecondaryAction(secAction, isFirstChild),
        container
      );
    } else if (!secAction && container) {
      item.removeChild(container);
    }

    item.removeAttribute("feedback-acknowledgment");

    if (
      result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
      !result.payload.providesSearchMode &&
      !result.payload.inPrivateWindow
    ) {
      item.setAttribute("type", "search");
    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
      item.setAttribute("type", "remotetab");
    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
      item.setAttribute("type", "switchtab");
    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP) {
      item.setAttribute("type", "tip");
      item.setAttribute("tip-type", result.payload.type);

      // Due to role=button, the button and help icon can sometimes become
      // focused. We want to prevent that because the input should always be
      // focused instead. (This happens when input.search("", { focus: false })
      // is called, a tip is the first result but not heuristic, and the user
--> --------------------

--> maximum size reached

--> --------------------

[ Verzeichnis aufwärts0.49unsichere Verbindung  Übersetzung europäischer Sprachen durch Browser  ]