tor-browser

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

sidebar-history.mjs (13182B)


      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 const lazy = {};
      6 
      7 import {
      8  classMap,
      9  html,
     10  ifDefined,
     11  when,
     12  nothing,
     13 } from "chrome://global/content/vendor/lit.all.mjs";
     14 import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs";
     15 
     16 import { SidebarPage } from "./sidebar-page.mjs";
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  HistoryController: "resource:///modules/HistoryController.sys.mjs",
     20  Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
     21  SidebarTreeView:
     22    "moz-src:///browser/components/sidebar/SidebarTreeView.sys.mjs",
     23  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     24  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     25 });
     26 
     27 const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
     28 const DAYS_EXPANDED_INITIALLY = 2;
     29 
     30 export class SidebarHistory extends SidebarPage {
     31  static queries = {
     32    cards: { all: "moz-card" },
     33    emptyState: "fxview-empty-state",
     34    lists: { all: "sidebar-tab-list" },
     35    menuButton: ".menu-button",
     36    searchTextbox: "moz-input-search",
     37  };
     38 
     39  constructor() {
     40    super();
     41    this.handlePopupEvent = this.handlePopupEvent.bind(this);
     42    this.controller = new lazy.HistoryController(this, {
     43      component: "sidebar",
     44    });
     45    this.treeView = new lazy.SidebarTreeView(this);
     46  }
     47 
     48  connectedCallback() {
     49    super.connectedCallback();
     50    const { document: doc } = this.topWindow;
     51    this._menu = doc.getElementById("sidebar-history-menu");
     52    this._menuSortByDate = doc.getElementById("sidebar-history-sort-by-date");
     53    this._menuSortBySite = doc.getElementById("sidebar-history-sort-by-site");
     54    this._menuSortByDateSite = doc.getElementById(
     55      "sidebar-history-sort-by-date-and-site"
     56    );
     57    this._menuSortByLastVisited = doc.getElementById(
     58      "sidebar-history-sort-by-last-visited"
     59    );
     60    this._menu.addEventListener("command", this);
     61    this._menu.addEventListener("popuphidden", this.handlePopupEvent);
     62    this._contextMenu.addEventListener("popupshowing", this);
     63    this.addContextMenuListeners();
     64    this.addSidebarFocusedListeners();
     65    this.controller.updateCache();
     66  }
     67 
     68  disconnectedCallback() {
     69    super.disconnectedCallback();
     70    this._menu.removeEventListener("command", this);
     71    this._menu.removeEventListener("popuphidden", this.handlePopupEvent);
     72    this._contextMenu.removeEventListener("popupshowing", this);
     73    this.removeContextMenuListeners();
     74    this.removeSidebarFocusedListeners();
     75  }
     76 
     77  handleEvent(e) {
     78    switch (e.type) {
     79      case "popupshowing":
     80        this.updateContextMenu();
     81        break;
     82      default:
     83        super.handleEvent(e);
     84    }
     85  }
     86 
     87  get isMultipleRowsSelected() {
     88    return !!this.treeView.selectedLists.size;
     89  }
     90 
     91  /**
     92   * Only show multiselect commands when multiple items are selected.
     93   */
     94  updateContextMenu() {
     95    for (const child of this._contextMenu.children) {
     96      const isMultiSelectCommand = child.classList.contains(
     97        "sidebar-history-multiselect-command"
     98      );
     99      if (this.isMultipleRowsSelected) {
    100        child.hidden = !isMultiSelectCommand;
    101      } else {
    102        child.hidden = isMultiSelectCommand;
    103      }
    104    }
    105    let privateWindowMenuItem = this._contextMenu.querySelector(
    106      "#sidebar-history-context-open-in-private-window"
    107    );
    108    privateWindowMenuItem.hidden = !lazy.PrivateBrowsingUtils.enabled;
    109  }
    110 
    111  handleContextMenuEvent(e) {
    112    this.triggerNode =
    113      this.findTriggerNode(e, "sidebar-tab-row") ||
    114      this.findTriggerNode(e, "moz-input-search");
    115    if (!this.triggerNode) {
    116      e.preventDefault();
    117    }
    118  }
    119 
    120  handleCommandEvent(e) {
    121    switch (e.target.id) {
    122      case "sidebar-history-sort-by-date":
    123        this.controller.onChangeSortOption(e, "date");
    124        break;
    125      case "sidebar-history-sort-by-site":
    126        this.controller.onChangeSortOption(e, "site");
    127        break;
    128      case "sidebar-history-sort-by-date-and-site":
    129        this.controller.onChangeSortOption(e, "datesite");
    130        break;
    131      case "sidebar-history-sort-by-last-visited":
    132        this.controller.onChangeSortOption(e, "lastvisited");
    133        break;
    134      case "sidebar-history-clear":
    135        lazy.Sanitizer.showUI(this.topWindow);
    136        break;
    137      case "sidebar-history-context-delete-page":
    138        this.controller.deleteFromHistory().catch(console.error);
    139        break;
    140      case "sidebar-history-context-delete-pages":
    141        this.#deleteMultipleFromHistory().catch(console.error);
    142        break;
    143      default:
    144        super.handleCommandEvent(e);
    145        break;
    146    }
    147  }
    148 
    149  #deleteMultipleFromHistory() {
    150    const pageGuids = [...this.treeView.selectedLists].flatMap(
    151      ({ selectedGuids }) => [...selectedGuids]
    152    );
    153    return lazy.PlacesUtils.history.remove(pageGuids);
    154  }
    155 
    156  // We should let moz-button handle this, see bug 1875374.
    157  handlePopupEvent(e) {
    158    if (e.type == "popuphidden") {
    159      this.menuButton.setAttribute("aria-expanded", false);
    160    }
    161  }
    162 
    163  handleSidebarFocusedEvent() {
    164    this.searchTextbox?.focus();
    165  }
    166 
    167  onPrimaryAction(e) {
    168    if (this.isMultipleRowsSelected) {
    169      // Avoid opening multiple links at once.
    170      return;
    171    }
    172    navigateToLink(e, e.originalTarget.url, { forceNewTab: false });
    173    this.treeView.clearSelection();
    174  }
    175 
    176  onSecondaryAction(e) {
    177    this.triggerNode = e.detail.item;
    178    this.controller.deleteFromHistory().catch(console.error);
    179  }
    180 
    181  /**
    182   * The template to use for cards-container.
    183   */
    184  get cardsTemplate() {
    185    if (this.controller.isHistoryPending) {
    186      // don't render cards until initial history visits entries are available
    187      return "";
    188    } else if (this.controller.searchResults) {
    189      return this.#searchResultsTemplate();
    190    } else if (!this.controller.isHistoryEmpty) {
    191      return this.#historyCardsTemplate();
    192    }
    193    return this.#emptyMessageTemplate();
    194  }
    195 
    196  #historyCardsTemplate() {
    197    const { historyVisits } = this.controller;
    198    switch (this.controller.sortOption) {
    199      case "date":
    200        return historyVisits.map(({ l10nId, items }, i) =>
    201          this.#dateCardTemplate(l10nId, i, items)
    202        );
    203      case "site":
    204        return historyVisits.map(({ domain, items }, i) =>
    205          this.#siteCardTemplate(domain, i, items)
    206        );
    207      case "datesite":
    208        return historyVisits.map(({ l10nId, items }, i) =>
    209          this.#dateCardTemplate(l10nId, i, items, true)
    210        );
    211      case "lastvisited":
    212        return historyVisits.map(
    213          ({ items }) =>
    214            html`<moz-card>
    215              ${this.#tabListTemplate(this.getTabItems(items))}
    216            </moz-card>`
    217        );
    218      default:
    219        return [];
    220    }
    221  }
    222 
    223  #dateCardTemplate(l10nId, index, items, isDateSite = false) {
    224    const tabIndex = index > 0 ? "-1" : undefined;
    225    return html` <moz-card
    226      type="accordion"
    227      class="date-card"
    228      ?expanded=${index < DAYS_EXPANDED_INITIALLY}
    229      data-l10n-id=${l10nId}
    230      data-l10n-args=${JSON.stringify({
    231        date: isDateSite ? items[0][1][0].time : items[0].time,
    232      })}
    233      @keydown=${e => this.treeView.handleCardKeydown(e)}
    234      tabindex=${ifDefined(tabIndex)}
    235    >
    236      ${isDateSite
    237        ? items.map(([domain, visits], i) =>
    238            this.#siteCardTemplate(
    239              domain,
    240              i,
    241              visits,
    242              true,
    243              i == items.length - 1
    244            )
    245          )
    246        : this.#tabListTemplate(this.getTabItems(items))}
    247    </moz-card>`;
    248  }
    249 
    250  #siteCardTemplate(
    251    domain,
    252    index,
    253    items,
    254    isDateSite = false,
    255    isLastCard = false
    256  ) {
    257    let tabIndex = index > 0 || isDateSite ? "-1" : undefined;
    258    return html` <moz-card
    259      class=${classMap({
    260        "last-card": isLastCard,
    261        "nested-card": isDateSite,
    262        "site-card": true,
    263      })}
    264      type="accordion"
    265      ?expanded=${!isDateSite}
    266      heading=${domain}
    267      @keydown=${e => this.treeView.handleCardKeydown(e)}
    268      tabindex=${ifDefined(tabIndex)}
    269      data-l10n-id=${domain ? nothing : "sidebar-history-site-localhost"}
    270      data-l10n-attrs=${domain ? nothing : "heading"}
    271    >
    272      ${this.#tabListTemplate(this.getTabItems(items))}
    273    </moz-card>`;
    274  }
    275 
    276  #emptyMessageTemplate() {
    277    let descriptionHeader;
    278    let descriptionLabels;
    279    let descriptionLink;
    280    if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
    281      // History pref set to never remember history
    282      descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
    283      descriptionLabels = [
    284        "firefoxview-dont-remember-history-empty-description-one",
    285      ];
    286      descriptionLink = {
    287        url: "about:preferences#privacy",
    288        name: "history-settings-url-two",
    289      };
    290    } else {
    291      descriptionHeader = "firefoxview-history-empty-header";
    292      descriptionLabels = [
    293        "firefoxview-history-empty-description",
    294        "firefoxview-history-empty-description-two",
    295      ];
    296      descriptionLink = {
    297        url: "about:preferences#privacy",
    298        name: "history-settings-url",
    299      };
    300    }
    301    return html`
    302      <fxview-empty-state
    303        headerLabel=${descriptionHeader}
    304        .descriptionLabels=${descriptionLabels}
    305        .descriptionLink=${descriptionLink}
    306        class="empty-state history"
    307        isSelectedTab
    308        mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
    309        openLinkInParentWindow
    310      >
    311      </fxview-empty-state>
    312    `;
    313  }
    314 
    315  #searchResultsTemplate() {
    316    return html` <moz-card
    317      data-l10n-id="sidebar-search-results-header"
    318      data-l10n-args=${JSON.stringify({
    319        query: this.controller.searchQuery,
    320      })}
    321    >
    322      <div>
    323        ${when(
    324          this.controller.searchResults.length,
    325          () =>
    326            html`<h3
    327              slot="secondary-header"
    328              data-l10n-id="firefoxview-search-results-count"
    329              data-l10n-args=${JSON.stringify({
    330                count: this.controller.searchResults.length,
    331              })}
    332            ></h3>`
    333        )}
    334        ${this.#tabListTemplate(
    335          this.getTabItems(this.controller.searchResults),
    336          this.controller.searchQuery
    337        )}
    338      </div>
    339    </moz-card>`;
    340  }
    341 
    342  #tabListTemplate(tabItems, searchQuery) {
    343    return html`<sidebar-tab-list
    344      .handleFocusElementToCard=${this.handleFocusElementToCard}
    345      maxTabsLength="-1"
    346      .searchQuery=${searchQuery}
    347      secondaryActionClass="delete-button"
    348      .sortOption=${this.controller.sortOption}
    349      .tabItems=${tabItems}
    350      @fxview-tab-list-primary-action=${this.onPrimaryAction}
    351      @fxview-tab-list-secondary-action=${this.onSecondaryAction}
    352    >
    353    </sidebar-tab-list>`;
    354  }
    355 
    356  onSearchQuery(e) {
    357    this.controller.onSearchQuery(e);
    358  }
    359 
    360  getTabItems(items) {
    361    return items.map(item => ({
    362      ...item,
    363      secondaryL10nId: "sidebar-history-delete",
    364      secondaryL10nArgs: null,
    365    }));
    366  }
    367 
    368  openMenu(e) {
    369    const menuPos = this.sidebarController._positionStart
    370      ? "after_start" // Sidebar is on the left. Open menu to the right.
    371      : "after_end"; // Sidebar is on the right. Open menu to the left.
    372    this._menu.openPopup(e.target, menuPos, 0, 0, false, false, e);
    373    this.menuButton.setAttribute("aria-expanded", true);
    374  }
    375 
    376  willUpdate() {
    377    this._menuSortByDate.setAttribute(
    378      "checked",
    379      this.controller.sortOption == "date"
    380    );
    381    this._menuSortBySite.setAttribute(
    382      "checked",
    383      this.controller.sortOption == "site"
    384    );
    385    this._menuSortByDateSite.setAttribute(
    386      "checked",
    387      this.controller.sortOption == "datesite"
    388    );
    389    this._menuSortByLastVisited.setAttribute(
    390      "checked",
    391      this.controller.sortOption == "lastvisited"
    392    );
    393  }
    394 
    395  render() {
    396    return html`
    397      ${this.stylesheet()}
    398      <link
    399        rel="stylesheet"
    400        href="chrome://browser/content/sidebar/sidebar-history.css"
    401      />
    402      <div class="sidebar-panel">
    403        <sidebar-panel-header
    404          data-l10n-id="sidebar-menu-history-header"
    405          data-l10n-attrs="heading"
    406          view="viewHistorySidebar"
    407        >
    408          <div class="options-container">
    409            <moz-input-search
    410              data-l10n-id="firefoxview-search-text-box-history"
    411              data-l10n-attrs="placeholder"
    412              @MozInputSearch:search=${this.onSearchQuery}
    413            ></moz-input-search>
    414            <moz-button
    415              class="menu-button"
    416              @click=${this.openMenu}
    417              data-l10n-id="sidebar-options-menu-button"
    418              aria-haspopup="menu"
    419              aria-expanded="false"
    420              view=${this.view}
    421              type="icon ghost"
    422              iconsrc="chrome://global/skin/icons/more.svg"
    423            >
    424            </moz-button>
    425          </div>
    426        </sidebar-panel-header>
    427        <div class="sidebar-panel-scrollable-content">
    428          ${this.cardsTemplate}
    429        </div>
    430      </div>
    431    `;
    432  }
    433 }
    434 
    435 customElements.define("sidebar-history", SidebarHistory);