tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sidebar-tab-list.mjs (9510B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import {
      6  classMap,
      7  html,
      8  ifDefined,
      9  when,
     10 } from "chrome://global/content/vendor/lit.all.mjs";
     11 
     12 import {
     13  FxviewTabListBase,
     14  FxviewTabRowBase,
     15 } from "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
     16 
     17 export class SidebarTabList extends FxviewTabListBase {
     18  constructor() {
     19    super();
     20    // Panel is open, assume we always want to react to updates.
     21    this.updatesPaused = false;
     22    this.multiSelect = true;
     23    this.selectedGuids = new Set();
     24    this.shortcutsLocalization = new Localization(
     25      ["toolkit/global/textActions.ftl"],
     26      true
     27    );
     28  }
     29 
     30  static queries = {
     31    ...FxviewTabListBase.queries,
     32    rowEls: {
     33      all: "sidebar-tab-row",
     34    },
     35  };
     36 
     37  /**
     38   * Only handle vertical navigation in sidebar.
     39   *
     40   * @param {KeyboardEvent} e
     41   */
     42  handleFocusElementInRow(e) {
     43    // Handle vertical navigation.
     44    if (
     45      (e.code == "ArrowUp" && this.activeIndex > 0) ||
     46      (e.code == "ArrowDown" && this.activeIndex < this.rowEls.length - 1)
     47    ) {
     48      super.handleFocusElementInRow(e);
     49    } else if (
     50      (e.code == "ArrowUp" && this.activeIndex == 0) ||
     51      e.code === "ArrowLeft"
     52    ) {
     53      this.#focusParentHeader(e.target);
     54    } else if (
     55      e.code == "ArrowDown" &&
     56      this.activeIndex == this.rowEls.length - 1
     57    ) {
     58      this.#focusNextHeader(e.target);
     59    }
     60 
     61    // Update or clear multi-selection (depending on whether shift key is used).
     62    if (this.multiSelect && (e.code === "ArrowUp" || e.code === "ArrowDown")) {
     63      this.#updateSelection(e);
     64    }
     65 
     66    // (Ctrl / Cmd) + A should select all rows.
     67    if (
     68      e.getModifierState("Accel") &&
     69      e.key.toUpperCase() === this.selectAllShortcut
     70    ) {
     71      e.preventDefault();
     72      this.#selectAll();
     73    }
     74  }
     75 
     76  #focusParentHeader(row) {
     77    let parentCard = row.getRootNode().host.closest("moz-card");
     78    if (parentCard) {
     79      parentCard.summaryEl.focus();
     80    }
     81  }
     82 
     83  #focusNextHeader(row) {
     84    let parentCard = row.getRootNode().host.closest("moz-card");
     85    if (
     86      this.sortOption == "datesite" &&
     87      parentCard.classList.contains("last-card")
     88    ) {
     89      // If we're going down from the last site, then focus the next date.
     90      const dateCard = parentCard.parentElement;
     91      const nextDate = dateCard.nextElementSibling;
     92      nextDate?.summaryEl.focus();
     93    }
     94    let nextCard = parentCard.nextElementSibling;
     95    if (nextCard && nextCard.localName == "moz-card") {
     96      nextCard.summaryEl.focus();
     97    }
     98  }
     99 
    100  #updateSelection(event) {
    101    if (!event.shiftKey) {
    102      // Clear the selection when navigating without shift key.
    103      // Dispatch event so that other lists will also clear their selection.
    104      this.clearSelection();
    105      this.dispatchEvent(
    106        new CustomEvent("clear-selection", {
    107          bubbles: true,
    108          composed: true,
    109        })
    110      );
    111      return;
    112    }
    113 
    114    // Select the current row.
    115    const row = event.target;
    116    const {
    117      guid,
    118      previousElementSibling: prevRow,
    119      nextElementSibling: nextRow,
    120    } = row;
    121    this.selectedGuids.add(guid);
    122 
    123    // Select the previous or next sibling, depending on which arrow key was used.
    124    if (event.code === "ArrowUp" && prevRow) {
    125      this.selectedGuids.add(prevRow.guid);
    126    } else if (event.code === "ArrowDown" && nextRow) {
    127      this.selectedGuids.add(nextRow.guid);
    128    } else {
    129      this.requestVirtualListUpdate();
    130    }
    131 
    132    // Notify the host component.
    133    this.dispatchEvent(
    134      new CustomEvent("update-selection", {
    135        bubbles: true,
    136        composed: true,
    137      })
    138    );
    139  }
    140 
    141  clearSelection() {
    142    this.selectedGuids.clear();
    143    this.requestVirtualListUpdate();
    144  }
    145 
    146  get selectAllShortcut() {
    147    const [l10nMessage] = this.shortcutsLocalization.formatMessagesSync([
    148      "text-action-select-all-shortcut",
    149    ]);
    150    const shortcutKey = l10nMessage.attributes[0].value;
    151    return shortcutKey;
    152  }
    153 
    154  #selectAll() {
    155    for (const { guid } of this.tabItems) {
    156      this.selectedGuids.add(guid);
    157    }
    158    this.requestVirtualListUpdate();
    159    this.dispatchEvent(
    160      new CustomEvent("update-selection", {
    161        bubbles: true,
    162        composed: true,
    163      })
    164    );
    165  }
    166 
    167  itemTemplate = (tabItem, i) => {
    168    let tabIndex = -1;
    169    if ((this.searchQuery || this.sortOption == "lastvisited") && i == 0) {
    170      // Make the first row focusable if there is no header.
    171      tabIndex = 0;
    172    } else if (!this.searchQuery) {
    173      tabIndex = 0;
    174    }
    175    return html`
    176      <sidebar-tab-row
    177        ?active=${i == this.activeIndex}
    178        .canClose=${ifDefined(tabItem.canClose)}
    179        .closedId=${ifDefined(tabItem.closedId)}
    180        compact
    181        .currentActiveElementId=${this.currentActiveElementId}
    182        .closeRequested=${tabItem.closeRequested}
    183        .containerObj=${tabItem.containerObj}
    184        .fxaDeviceId=${ifDefined(tabItem.fxaDeviceId)}
    185        .favicon=${tabItem.icon}
    186        .guid=${tabItem.guid}
    187        .hasPopup=${this.hasPopup}
    188        .indicators=${tabItem.indicators}
    189        .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
    190        .primaryL10nId=${tabItem.primaryL10nId}
    191        role="listitem"
    192        .searchQuery=${ifDefined(this.searchQuery)}
    193        .secondaryActionClass=${ifDefined(
    194          this.secondaryActionClass ?? tabItem.secondaryActionClass
    195        )}
    196        .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
    197        .secondaryL10nId=${tabItem.secondaryL10nId}
    198        .selected=${this.selectedGuids.has(tabItem.guid)}
    199        .sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
    200        .sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
    201        .tabElement=${ifDefined(tabItem.tabElement)}
    202        tabindex=${tabIndex}
    203        .title=${tabItem.title}
    204        .url=${tabItem.url}
    205        @keydown=${e => e.currentTarget.primaryActionHandler(e)}
    206      ></sidebar-tab-row>
    207    `;
    208  };
    209 
    210  stylesheets() {
    211    return [
    212      super.stylesheets(),
    213      html`<link
    214        rel="stylesheet"
    215        href="chrome://browser/content/sidebar/sidebar-tab-list.css"
    216      />`,
    217    ];
    218  }
    219 }
    220 customElements.define("sidebar-tab-list", SidebarTabList);
    221 
    222 export class SidebarTabRow extends FxviewTabRowBase {
    223  static properties = {
    224    containerObj: { type: Object },
    225    guid: { type: String },
    226    selected: { type: Boolean, reflect: true },
    227    indicators: { type: Array },
    228  };
    229 
    230  /**
    231   * Fallback to the native implementation in sidebar. We want to focus the
    232   * entire row instead of delegating it to link or hover buttons.
    233   */
    234  focus() {
    235    HTMLElement.prototype.focus.call(this);
    236  }
    237 
    238  #getContainerClasses() {
    239    let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
    240    if (this.containerObj) {
    241      let { icon, color } = this.containerObj;
    242      containerClasses.push(`identity-icon-${icon}`);
    243      containerClasses.push(`identity-color-${color}`);
    244    }
    245    return containerClasses;
    246  }
    247 
    248  #containerIndicatorTemplate() {
    249    let tabList = this.getRootNode().host;
    250    let tabsToCheck = tabList.tabItems;
    251    return html`${when(
    252      tabsToCheck.some(tab => tab.containerObj),
    253      () => html`<span class=${this.#getContainerClasses().join(" ")}></span>`
    254    )}`;
    255  }
    256 
    257  secondaryButtonTemplate() {
    258    return html`${when(
    259      this.secondaryL10nId && this.secondaryActionClass,
    260      () =>
    261        html`<moz-button
    262          aria-haspopup=${ifDefined(this.hasPopup)}
    263          class=${classMap({
    264            "fxview-tab-row-button": true,
    265            [this.secondaryActionClass]: this.secondaryActionClass,
    266          })}
    267          data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
    268          data-l10n-id=${this.secondaryL10nId}
    269          id="fxview-tab-row-secondary-button"
    270          type="icon ghost"
    271          @click=${this.secondaryActionHandler}
    272          iconSrc=${this.getIconSrc(this.secondaryActionClass)}
    273        ></moz-button>`
    274    )}`;
    275  }
    276 
    277  render() {
    278    return html`
    279      ${this.stylesheets()}
    280      ${when(
    281        this.containerObj,
    282        () => html`
    283          <link
    284            rel="stylesheet"
    285            href="chrome://browser/content/usercontext/usercontext.css"
    286          />
    287        `
    288      )}
    289      <link
    290        rel="stylesheet"
    291        href="chrome://browser/content/sidebar/sidebar-tab-row.css"
    292      />
    293      <a
    294        class=${classMap({
    295          "fxview-tab-row-main": true,
    296          "no-action-button-row": this.canClose === false,
    297          muted: this.indicators?.includes("muted"),
    298          attention: this.indicators?.includes("attention"),
    299          soundplaying: this.indicators?.includes("soundplaying"),
    300          "activemedia-blocked": this.indicators?.includes(
    301            "activemedia-blocked"
    302          ),
    303        })}
    304        ?disabled=${this.closeRequested}
    305        data-l10n-args=${ifDefined(this.primaryL10nArgs)}
    306        data-l10n-id=${ifDefined(this.primaryL10nId)}
    307        href=${ifDefined(this.url)}
    308        id="fxview-tab-row-main"
    309        tabindex="-1"
    310        title=${!this.primaryL10nId ? this.url : null}
    311        @click=${this.primaryActionHandler}
    312        @keydown=${this.primaryActionHandler}
    313      >
    314        ${this.faviconTemplate()} ${this.titleTemplate()}
    315      </a>
    316      ${this.secondaryButtonTemplate()} ${this.#containerIndicatorTemplate()}
    317    `;
    318  }
    319 }
    320 customElements.define("sidebar-tab-row", SidebarTabRow);