tor-browser

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

tabbing-order.js (6826B)


      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 const lazy = {};
      8 loader.lazyGetter(
      9  lazy,
     10  "ContentDOMReference",
     11  () =>
     12    ChromeUtils.importESModule(
     13      "resource://gre/modules/ContentDOMReference.sys.mjs",
     14      // ContentDOMReference needs to be retrieved from the shared global
     15      // since it is a shared singleton.
     16      { global: "shared" }
     17    ).ContentDOMReference
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  ["isFrameWithChildTarget", "isWindowIncluded"],
     22  "resource://devtools/shared/layout/utils.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "NodeTabbingOrderHighlighter",
     28  "resource://devtools/server/actors/highlighters/node-tabbing-order.js",
     29  true
     30 );
     31 
     32 const DEFAULT_FOCUS_FLAGS = Services.focus.FLAG_NOSCROLL;
     33 
     34 /**
     35 * The TabbingOrderHighlighter uses focus manager to traverse all focusable
     36 * nodes on the page and then uses the NodeTabbingOrderHighlighter to highlight
     37 * these nodes.
     38 */
     39 class TabbingOrderHighlighter {
     40  constructor(highlighterEnv) {
     41    this.highlighterEnv = highlighterEnv;
     42    this._highlighters = new Map();
     43 
     44    this.onMutation = this.onMutation.bind(this);
     45    this.onPageHide = this.onPageHide.bind(this);
     46    this.onWillNavigate = this.onWillNavigate.bind(this);
     47 
     48    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
     49 
     50    const { pageListenerTarget } = highlighterEnv;
     51    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
     52  }
     53 
     54  /**
     55   * Static getter that indicates that TabbingOrderHighlighter supports
     56   * highlighting in XUL windows.
     57   */
     58  static get XULSupported() {
     59    return true;
     60  }
     61 
     62  get win() {
     63    return this.highlighterEnv.window;
     64  }
     65 
     66  get focusedElement() {
     67    return Services.focus.getFocusedElementForWindow(this.win, true, {});
     68  }
     69 
     70  set focusedElement(element) {
     71    Services.focus.setFocus(element, DEFAULT_FOCUS_FLAGS);
     72  }
     73 
     74  moveFocus(startElement) {
     75    return Services.focus.moveFocus(
     76      this.win,
     77      startElement.nodeType === Node.DOCUMENT_NODE
     78        ? startElement.documentElement
     79        : startElement,
     80      Services.focus.MOVEFOCUS_FORWARD,
     81      DEFAULT_FOCUS_FLAGS
     82    );
     83  }
     84 
     85  /**
     86   * Show NodeTabbingOrderHighlighter on each node that belongs to the keyboard
     87   * tabbing order.
     88   *
     89   * @param  {DOMNode} startElm
     90   *         Starting element to calculate tabbing order from.
     91   *
     92   * @param  {JSON} options
     93   *         - options.index
     94   *           Start index for the tabbing order. Starting index will be 0 at
     95   *           the start of the tabbing order highlighting; in remote frames
     96   *           starting index will, typically, be greater than 0 (unless there
     97   *           was nothing to focus in the top level content document prior to
     98   *           the remote frame).
     99   */
    100  async show(startElm, { index }) {
    101    const focusableElements = [];
    102    const originalFocusedElement = this.focusedElement;
    103    let currentFocusedElement = this.moveFocus(startElm);
    104    while (
    105      currentFocusedElement &&
    106      isWindowIncluded(this.win, currentFocusedElement.ownerGlobal)
    107    ) {
    108      focusableElements.push(currentFocusedElement);
    109      currentFocusedElement = this.moveFocus(currentFocusedElement);
    110    }
    111 
    112    // Allow to flush pending notifications to ensure the PresShell and frames
    113    // are updated.
    114    await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
    115    let endElm = this.focusedElement;
    116    if (
    117      currentFocusedElement &&
    118      !isWindowIncluded(this.win, currentFocusedElement.ownerGlobal)
    119    ) {
    120      endElm = null;
    121    }
    122 
    123    if (
    124      !endElm &&
    125      !!focusableElements.length &&
    126      isFrameWithChildTarget(
    127        this.highlighterEnv.targetActor,
    128        focusableElements[focusableElements.length - 1]
    129      )
    130    ) {
    131      endElm = focusableElements[focusableElements.length - 1];
    132    }
    133 
    134    if (originalFocusedElement && originalFocusedElement !== endElm) {
    135      this.focusedElement = originalFocusedElement;
    136    }
    137 
    138    const highlighters = [];
    139    for (let i = 0; i < focusableElements.length; i++) {
    140      highlighters.push(
    141        this._accumulateHighlighter(focusableElements[i], index++)
    142      );
    143    }
    144    await Promise.all(highlighters);
    145 
    146    this._trackMutations();
    147 
    148    return {
    149      contentDOMReference: endElm && lazy.ContentDOMReference.get(endElm),
    150      index,
    151    };
    152  }
    153 
    154  async _accumulateHighlighter(node, index) {
    155    const highlighter = new NodeTabbingOrderHighlighter(this.highlighterEnv);
    156    await highlighter.isReady;
    157 
    158    highlighter.show(node, { index: index + 1 });
    159    this._highlighters.set(node, highlighter);
    160  }
    161 
    162  hide() {
    163    this._untrackMutations();
    164    for (const highlighter of this._highlighters.values()) {
    165      highlighter.destroy();
    166    }
    167 
    168    this._highlighters.clear();
    169  }
    170 
    171  /**
    172   * Track mutations in the top level document subtree so that the appropriate
    173   * NodeTabbingOrderHighlighter infobar's could be updated to reflect the
    174   * attribute mutations on relevant nodes.
    175   */
    176  _trackMutations() {
    177    const { win } = this;
    178    this.currentMutationObserver = new win.MutationObserver(this.onMutation);
    179    this.currentMutationObserver.observe(win.document.documentElement, {
    180      subtree: true,
    181      attributes: true,
    182    });
    183  }
    184 
    185  _untrackMutations() {
    186    if (!this.currentMutationObserver) {
    187      return;
    188    }
    189 
    190    this.currentMutationObserver.disconnect();
    191    this.currentMutationObserver = null;
    192  }
    193 
    194  onMutation(mutationList) {
    195    for (const { target } of mutationList) {
    196      const highlighter = this._highlighters.get(target);
    197      if (highlighter) {
    198        highlighter.update();
    199      }
    200    }
    201  }
    202 
    203  /**
    204   * Update NodeTabbingOrderHighlighter focus styling for a node that,
    205   * potentially, belongs to the tabbing order.
    206   *
    207   * @param {object} options
    208   *        Options specifying the node and its focused state.
    209   */
    210  updateFocus({ node, focused }) {
    211    const highlighter = this._highlighters.get(node);
    212    if (!highlighter) {
    213      return;
    214    }
    215 
    216    highlighter.updateFocus(focused);
    217  }
    218 
    219  destroy() {
    220    this.highlighterEnv.off("will-navigate", this.onWillNavigate);
    221 
    222    const { pageListenerTarget } = this.highlighterEnv;
    223    if (pageListenerTarget) {
    224      pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
    225    }
    226 
    227    this.hide();
    228    this.highlighterEnv = null;
    229  }
    230 
    231  onPageHide({ target }) {
    232    // If a pagehide event is triggered for current window's highlighter, hide
    233    // the highlighter.
    234    if (target.defaultView === this.win) {
    235      this.hide();
    236    }
    237  }
    238 
    239  onWillNavigate({ isTopLevel }) {
    240    if (isTopLevel) {
    241      this.hide();
    242    }
    243  }
    244 }
    245 
    246 exports.TabbingOrderHighlighter = TabbingOrderHighlighter;