tor-browser

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

tabsplitview.js (8127B)


      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 "use strict";
      6 
      7 // This is loaded into chrome windows with the subscript loader. Wrap in
      8 // a block to prevent accidentally leaking globals onto `window`.
      9 {
     10  ChromeUtils.defineESModuleGetters(this, {
     11    DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     12  });
     13 
     14  /**
     15   * A shared task which updates the urlbar indicator whenever:
     16   * - A split view is activated or deactivated.
     17   * - The active tab of a split view changes.
     18   * - The order of tabs in a split view changes.
     19   *
     20   * @type {DeferredTask}
     21   */
     22  const updateUrlbarButton = new DeferredTask(() => {
     23    const { activeSplitView, selectedTab } = gBrowser;
     24    const button = document.getElementById("split-view-button");
     25    if (activeSplitView) {
     26      const activeIndex = activeSplitView.tabs.indexOf(selectedTab);
     27      button.hidden = false;
     28      button.setAttribute("data-active-index", activeIndex);
     29    } else {
     30      button.hidden = true;
     31      button.removeAttribute("data-active-index");
     32    }
     33  }, 0);
     34 
     35  class MozTabSplitViewWrapper extends MozXULElement {
     36    /** @type {MutationObserver} */
     37    #tabChangeObserver;
     38 
     39    /** @type {MozTabbrowserTab[]} */
     40    #tabs = [];
     41 
     42    #storedPanelWidths = new WeakMap();
     43 
     44    /**
     45     * @returns {boolean}
     46     */
     47    get hasActiveTab() {
     48      return this.hasAttribute("hasactivetab");
     49    }
     50 
     51    /**
     52     * @returns {MozTabbrowserGroup}
     53     */
     54    get group() {
     55      return gBrowser.isTabGroup(this.parentElement)
     56        ? this.parentElement
     57        : null;
     58    }
     59 
     60    /**
     61     * @param {boolean} val
     62     */
     63    set hasActiveTab(val) {
     64      this.toggleAttribute("hasactivetab", val);
     65    }
     66 
     67    constructor() {
     68      super();
     69    }
     70 
     71    connectedCallback() {
     72      // Set up TabSelect listener, as this gets
     73      // removed in disconnectedCallback
     74      this.ownerGlobal.addEventListener("TabSelect", this);
     75 
     76      this.#observeTabChanges();
     77      this.#restorePanelWidths();
     78 
     79      if (this.hasActiveTab) {
     80        this.#activate();
     81      }
     82 
     83      if (this._initialized) {
     84        return;
     85      }
     86 
     87      this._initialized = true;
     88 
     89      this.textContent = "";
     90 
     91      // Mirroring MozTabbrowserTab
     92      this.container = gBrowser.tabContainer;
     93    }
     94 
     95    disconnectedCallback() {
     96      this.#tabChangeObserver?.disconnect();
     97      this.ownerGlobal.removeEventListener("TabSelect", this);
     98      this.#deactivate();
     99      this.#resetPanelWidths();
    100      this.container.dispatchEvent(
    101        new CustomEvent("SplitViewRemoved", {
    102          bubbles: true,
    103          composed: true,
    104        })
    105      );
    106    }
    107 
    108    #observeTabChanges() {
    109      if (!this.#tabChangeObserver) {
    110        this.#tabChangeObserver = new window.MutationObserver(() => {
    111          if (this.tabs.length) {
    112            this.hasActiveTab = this.tabs.some(tab => tab.selected);
    113            this.tabs.forEach((tab, index) => {
    114              // Renumber tabs so that a11y tools can tell users that a given
    115              // tab is "1 of 2" in the split view, for example.
    116              tab.setAttribute("aria-posinset", index + 1);
    117              tab.setAttribute("aria-setsize", this.tabs.length);
    118              tab.updateSplitViewAriaLabel(index);
    119            });
    120          } else {
    121            this.remove();
    122          }
    123 
    124          if (this.tabs.length < 2) {
    125            this.unsplitTabs();
    126          }
    127        });
    128      }
    129      this.#tabChangeObserver.observe(this, {
    130        childList: true,
    131      });
    132    }
    133 
    134    get splitViewId() {
    135      return this.getAttribute("splitViewId");
    136    }
    137 
    138    set splitViewId(val) {
    139      this.setAttribute("splitViewId", val);
    140    }
    141 
    142    /**
    143     * @returns {MozTabbrowserTab[]}
    144     */
    145    get tabs() {
    146      return Array.from(this.children).filter(node => node.matches("tab"));
    147    }
    148 
    149    get visible() {
    150      return this.tabs.every(tab => tab.visible);
    151    }
    152 
    153    /**
    154     * Get the list of tab panels from this split view.
    155     *
    156     * @returns {XULElement[]}
    157     */
    158    get panels() {
    159      const panels = [];
    160      for (const { linkedPanel } of this.#tabs) {
    161        const el = document.getElementById(linkedPanel);
    162        if (el) {
    163          panels.push(el);
    164        }
    165      }
    166      return panels;
    167    }
    168 
    169    /**
    170     * Show all Split View tabs in the content area.
    171     */
    172    #activate(skipShowPanels = false) {
    173      updateUrlbarButton.arm();
    174      if (!skipShowPanels) {
    175        gBrowser.showSplitViewPanels(this.#tabs);
    176      }
    177      this.container.dispatchEvent(
    178        new CustomEvent("TabSplitViewActivate", {
    179          detail: { tabs: this.#tabs, splitview: this },
    180          bubbles: true,
    181        })
    182      );
    183    }
    184 
    185    /**
    186     * Remove Split View tabs from the content area.
    187     */
    188    #deactivate(skipHidePanels = false) {
    189      if (!skipHidePanels) {
    190        gBrowser.hideSplitViewPanels(this.#tabs);
    191      }
    192      updateUrlbarButton.arm();
    193      this.container.dispatchEvent(
    194        new CustomEvent("TabSplitViewDeactivate", {
    195          detail: { tabs: this.#tabs, splitview: this },
    196          bubbles: true,
    197        })
    198      );
    199    }
    200 
    201    /**
    202     * Remove customized panel widths. Cache width values so that they can be
    203     * restored if this Split View is later reactivated.
    204     */
    205    #resetPanelWidths() {
    206      for (const panel of this.panels) {
    207        const width = panel.getAttribute("width");
    208        if (width) {
    209          this.#storedPanelWidths.set(panel, width);
    210          panel.removeAttribute("width");
    211          panel.style.removeProperty("width");
    212        }
    213      }
    214    }
    215 
    216    /**
    217     * Resize panel widths back to cached values.
    218     */
    219    #restorePanelWidths() {
    220      for (const panel of this.panels) {
    221        const width = this.#storedPanelWidths.get(panel);
    222        if (width) {
    223          panel.setAttribute("width", width);
    224          panel.style.setProperty("width", width + "px");
    225        }
    226      }
    227    }
    228 
    229    /**
    230     * add tabs to the split view wrapper
    231     *
    232     * @param {MozTabbrowserTab[]} tabs
    233     */
    234    addTabs(tabs) {
    235      for (let tab of tabs) {
    236        if (tab.pinned) {
    237          return;
    238        }
    239        let tabToMove =
    240          this.ownerGlobal === tab.ownerGlobal
    241            ? tab
    242            : gBrowser.adoptTab(tab, {
    243                tabIndex: gBrowser.tabs.at(-1)._tPos + 1,
    244                selectTab: tab.selected,
    245              });
    246        this.#tabs.push(tabToMove);
    247        gBrowser.moveTabToSplitView(tabToMove, this);
    248        if (tab === gBrowser.selectedTab) {
    249          this.hasActiveTab = true;
    250        }
    251      }
    252      if (this.hasActiveTab) {
    253        this.#activate();
    254        gBrowser.setIsSplitViewActive(true, this.#tabs);
    255      }
    256    }
    257 
    258    /**
    259     * Remove all tabs from the split view wrapper and delete the split view.
    260     */
    261    unsplitTabs() {
    262      gBrowser.unsplitTabs(this);
    263      gBrowser.setIsSplitViewActive(false, this.#tabs);
    264    }
    265 
    266    /**
    267     * Replace a tab in the split view with another tab
    268     */
    269    replaceTab(tabToReplace, newTab) {
    270      this.#tabs = this.#tabs.filter(tab => tab != tabToReplace);
    271      this.addTabs([newTab]);
    272      gBrowser.removeTab(tabToReplace);
    273    }
    274 
    275    /**
    276     * Reverse order of the tabs in the split view wrapper.
    277     */
    278    reverseTabs() {
    279      const [firstTab, secondTab] = this.#tabs;
    280      gBrowser.moveTabBefore(secondTab, firstTab);
    281      this.#tabs = [secondTab, firstTab];
    282      gBrowser.showSplitViewPanels(this.#tabs);
    283      updateUrlbarButton.arm();
    284    }
    285 
    286    /**
    287     * Close all tabs in the split view wrapper and delete the split view.
    288     */
    289    close() {
    290      gBrowser.removeTabs(this.#tabs);
    291    }
    292 
    293    /**
    294     * @param {CustomEvent} event
    295     */
    296    on_TabSelect(event) {
    297      this.hasActiveTab = event.target.splitview === this;
    298      gBrowser.setIsSplitViewActive(this.hasActiveTab, this.#tabs);
    299      if (this.hasActiveTab) {
    300        this.#activate();
    301      } else {
    302        this.#deactivate(true);
    303      }
    304    }
    305  }
    306 
    307  customElements.define("tab-split-view-wrapper", MozTabSplitViewWrapper);
    308 }