tor-browser

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

overlayHelpers.mjs (13545B)


      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 // An autoselection smaller than these will be ignored entirely:
      6 const MIN_DETECT_ABSOLUTE_HEIGHT = 10;
      7 const MIN_DETECT_ABSOLUTE_WIDTH = 30;
      8 // An autoselection smaller than these will not be preferred:
      9 const MIN_DETECT_HEIGHT = 30;
     10 const MIN_DETECT_WIDTH = 100;
     11 // An autoselection bigger than either of these will be ignored:
     12 let MAX_DETECT_HEIGHT = 700;
     13 let MAX_DETECT_WIDTH = 1000;
     14 
     15 const doNotAutoselectTags = {
     16  H1: true,
     17  H2: true,
     18  H3: true,
     19  H4: true,
     20  H5: true,
     21  H6: true,
     22 };
     23 
     24 /**
     25 * Gets the rect for an element if getBoundingClientRect exists
     26 *
     27 * @param ele The element to get the rect from
     28 * @returns The bounding client rect of the element or null
     29 */
     30 function getBoundingClientRect(ele) {
     31  if (!ele.getBoundingClientRect) {
     32    return null;
     33  }
     34 
     35  return ele.getBoundingClientRect();
     36 }
     37 
     38 export function setMaxDetectHeight(maxHeight) {
     39  MAX_DETECT_HEIGHT = maxHeight;
     40 }
     41 
     42 export function setMaxDetectWidth(maxWidth) {
     43  MAX_DETECT_WIDTH = maxWidth;
     44 }
     45 
     46 /**
     47 * This function will try to get an element from a given point in the doc.
     48 * This function is recursive because when sending a message to the
     49 * ScreenshotsHelper, the ScreenshotsHelper will call into this function.
     50 * This only occurs when the element at the given point is an iframe.
     51 *
     52 * If the element is an iframe, we will send a message to the ScreenshotsHelper
     53 * actor in the correct context to get the element at the given point.
     54 * The message will return the "getBestRectForElement" for the element at the
     55 * given point.
     56 *
     57 * If the element is not an iframe, then we will just return the element.
     58 *
     59 * @param {number} x The x coordinate
     60 * @param {number} y The y coordinate
     61 * @param {Document} doc The document
     62 * @returns {object}
     63 *    ele: The element for a given point (x, y)
     64 *    rect: The rect for the given point if ele is an iframe
     65 *          otherwise null
     66 */
     67 export async function getElementFromPoint(x, y, doc) {
     68  let ele = null;
     69  let rect = null;
     70  try {
     71    ele = doc.elementFromPoint(x, y);
     72    // if the element is an iframe, we need to send a message to that browsing context
     73    // to get the coordinates of the element in the iframe
     74    if (doc.defaultView.HTMLIFrameElement.isInstance(ele)) {
     75      let actor =
     76        ele.browsingContext.parentWindowContext.windowGlobalChild.getActor(
     77          "ScreenshotsHelper"
     78        );
     79      rect = await actor.sendQuery(
     80        "ScreenshotsHelper:GetElementRectFromPoint",
     81        {
     82          x: x + ele.ownerGlobal.mozInnerScreenX,
     83          y: y + ele.ownerGlobal.mozInnerScreenY,
     84          bcId: ele.browsingContext.id,
     85        }
     86      );
     87 
     88      if (rect) {
     89        rect = {
     90          left: rect.left - ele.ownerGlobal.mozInnerScreenX,
     91          right: rect.right - ele.ownerGlobal.mozInnerScreenX,
     92          top: rect.top - ele.ownerGlobal.mozInnerScreenY,
     93          bottom: rect.bottom - ele.ownerGlobal.mozInnerScreenY,
     94        };
     95      }
     96    } else if (ele.openOrClosedShadowRoot) {
     97      while (ele.openOrClosedShadowRoot) {
     98        let shadowEle = ele.openOrClosedShadowRoot.elementFromPoint(x, y);
     99        if (shadowEle) {
    100          ele = shadowEle;
    101        } else {
    102          break;
    103        }
    104      }
    105    }
    106  } catch (e) {
    107    console.error(e);
    108  }
    109 
    110  return { ele, rect };
    111 }
    112 
    113 /**
    114 * This function takes an element and finds a suitable rect to draw the hover box on
    115 *
    116 * @param {Element} ele The element to find a suitale rect of
    117 * @param {Document} doc The current document
    118 * @returns A suitable rect or null
    119 */
    120 export function getBestRectForElement(ele, doc) {
    121  let lastRect;
    122  let lastNode;
    123  let rect;
    124  let attemptExtend = false;
    125  let node = ele;
    126  while (node) {
    127    rect = getBoundingClientRect(node);
    128    if (!rect) {
    129      rect = lastRect;
    130      break;
    131    }
    132    if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) {
    133      // Avoid infinite loop for elements with zero or nearly zero height,
    134      // like non-clearfixed float parents with or without borders.
    135      break;
    136    }
    137    if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) {
    138      // Then the last rectangle is better
    139      rect = lastRect;
    140      attemptExtend = true;
    141      break;
    142    }
    143    if (rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT) {
    144      if (!doNotAutoselectTags[node.tagName]) {
    145        break;
    146      }
    147    }
    148    lastRect = rect;
    149    lastNode = node;
    150    node = node.parentNode;
    151  }
    152  if (rect && node) {
    153    const evenBetter = evenBetterElement(node, doc);
    154    if (evenBetter) {
    155      node = lastNode = evenBetter;
    156      rect = getBoundingClientRect(evenBetter);
    157      attemptExtend = false;
    158    }
    159  }
    160  if (rect && attemptExtend) {
    161    let extendNode = lastNode.nextSibling;
    162    while (extendNode) {
    163      if (extendNode.nodeType === doc.ELEMENT_NODE) {
    164        break;
    165      }
    166      extendNode = extendNode.nextSibling;
    167      if (!extendNode) {
    168        const parentNode = lastNode.parentNode;
    169        for (let i = 0; i < parentNode.childNodes.length; i++) {
    170          if (parentNode.childNodes[i] === lastNode) {
    171            extendNode = parentNode.childNodes[i + 1];
    172          }
    173        }
    174      }
    175    }
    176    if (extendNode) {
    177      const extendRect = getBoundingClientRect(extendNode);
    178      let x = Math.min(rect.x, extendRect.x);
    179      let y = Math.min(rect.y, extendRect.y);
    180      let width = Math.max(rect.right, extendRect.right) - x;
    181      let height = Math.max(rect.bottom, extendRect.bottom) - y;
    182      const combinedRect = new DOMRect(x, y, width, height);
    183      if (
    184        combinedRect.width <= MAX_DETECT_WIDTH &&
    185        combinedRect.height <= MAX_DETECT_HEIGHT
    186      ) {
    187        rect = combinedRect;
    188      }
    189    }
    190  }
    191 
    192  if (
    193    rect &&
    194    (rect.width < MIN_DETECT_ABSOLUTE_WIDTH ||
    195      rect.height < MIN_DETECT_ABSOLUTE_HEIGHT)
    196  ) {
    197    rect = null;
    198  }
    199 
    200  return rect;
    201 }
    202 
    203 /**
    204 * This finds a better element by looking for elements with role article
    205 *
    206 * @param {Element} node The currently hovered node
    207 * @param {Document} doc The current document
    208 * @returns A better node or null
    209 */
    210 function evenBetterElement(node, doc) {
    211  let el = node.parentNode;
    212  const ELEMENT_NODE = doc.ELEMENT_NODE;
    213  while (el && el.nodeType === ELEMENT_NODE) {
    214    if (!el.getAttribute) {
    215      return null;
    216    }
    217    if (el.getAttribute("role") === "article") {
    218      const rect = getBoundingClientRect(el);
    219      if (!rect) {
    220        return null;
    221      }
    222      if (rect.width <= MAX_DETECT_WIDTH && rect.height <= MAX_DETECT_HEIGHT) {
    223        return el;
    224      }
    225      return null;
    226    }
    227    el = el.parentNode;
    228  }
    229  return null;
    230 }
    231 
    232 export class Region {
    233  #x1;
    234  #x2;
    235  #y1;
    236  #y2;
    237  #xOffset;
    238  #yOffset;
    239  #windowDimensions;
    240 
    241  constructor(windowDimensions) {
    242    this.resetDimensions();
    243    this.#windowDimensions = windowDimensions;
    244  }
    245 
    246  /**
    247   * Sets the dimensions if the given dimension is defined.
    248   * Otherwise will reset the dimensions
    249   *
    250   * @param {object} dims The new region dimensions
    251   *  {
    252   *    left: new left dimension value or undefined
    253   *    top: new top dimension value or undefined
    254   *    right: new right dimension value or undefined
    255   *    bottom: new bottom dimension value or undefined
    256   *   }
    257   */
    258  set dimensions(dims) {
    259    if (dims == null) {
    260      this.resetDimensions();
    261      return;
    262    }
    263 
    264    if (dims.left != null) {
    265      this.left = dims.left;
    266    }
    267    if (dims.top != null) {
    268      this.top = dims.top;
    269    }
    270    if (dims.right != null) {
    271      this.right = dims.right;
    272    }
    273    if (dims.bottom != null) {
    274      this.bottom = dims.bottom;
    275    }
    276  }
    277 
    278  get dimensions() {
    279    return {
    280      left: this.left,
    281      top: this.top,
    282      right: this.right,
    283      bottom: this.bottom,
    284      width: this.width,
    285      height: this.height,
    286    };
    287  }
    288 
    289  get isRegionValid() {
    290    return this.#x1 + this.#x2 + this.#y1 + this.#y2 > 0;
    291  }
    292 
    293  resetDimensions() {
    294    this.#x1 = 0;
    295    this.#x2 = 0;
    296    this.#y1 = 0;
    297    this.#y2 = 0;
    298    this.#xOffset = 0;
    299    this.#yOffset = 0;
    300  }
    301 
    302  /**
    303   * Sort the coordinates so x1 < x2 and y1 < y2
    304   */
    305  sortCoords() {
    306    if (this.#x1 > this.#x2) {
    307      [this.#x1, this.#x2] = [this.#x2, this.#x1];
    308    }
    309    if (this.#y1 > this.#y2) {
    310      [this.#y1, this.#y2] = [this.#y2, this.#y1];
    311    }
    312  }
    313 
    314  /**
    315   * The region should never appear outside the document so the region will
    316   * be shifted if the region is outside the page's width or height.
    317   */
    318  shift() {
    319    let didShift = false;
    320    let xDiff = this.right - this.#windowDimensions.scrollWidth;
    321    if (xDiff > 0) {
    322      this.left -= xDiff;
    323      this.right -= xDiff;
    324 
    325      didShift = true;
    326    }
    327 
    328    let yDiff = this.bottom - this.#windowDimensions.scrollHeight;
    329    if (yDiff > 0) {
    330      this.top -= yDiff;
    331      this.bottom -= yDiff;
    332 
    333      didShift = true;
    334    }
    335 
    336    return didShift;
    337  }
    338 
    339  /**
    340   * The diagonal distance of the region
    341   */
    342  get distance() {
    343    return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2));
    344  }
    345 
    346  get xOffset() {
    347    return this.#xOffset;
    348  }
    349  set xOffset(val) {
    350    this.#xOffset = val;
    351  }
    352 
    353  get yOffset() {
    354    return this.#yOffset;
    355  }
    356  set yOffset(val) {
    357    this.#yOffset = val;
    358  }
    359 
    360  get top() {
    361    return Math.min(this.#y1, this.#y2);
    362  }
    363  set top(val) {
    364    this.#y1 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val));
    365  }
    366 
    367  get left() {
    368    return Math.min(this.#x1, this.#x2);
    369  }
    370  set left(val) {
    371    this.#x1 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val));
    372  }
    373 
    374  get right() {
    375    return Math.max(this.#x1, this.#x2);
    376  }
    377  set right(val) {
    378    this.#x2 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val));
    379  }
    380 
    381  get bottom() {
    382    return Math.max(this.#y1, this.#y2);
    383  }
    384  set bottom(val) {
    385    this.#y2 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val));
    386  }
    387 
    388  get width() {
    389    return Math.abs(this.#x2 - this.#x1);
    390  }
    391  get height() {
    392    return Math.abs(this.#y2 - this.#y1);
    393  }
    394 
    395  get x1() {
    396    return this.#x1;
    397  }
    398  get x2() {
    399    return this.#x2;
    400  }
    401  get y1() {
    402    return this.#y1;
    403  }
    404  get y2() {
    405    return this.#y2;
    406  }
    407 }
    408 
    409 export class WindowDimensions {
    410  #clientHeight = null;
    411  #clientWidth = null;
    412  #scrollHeight = null;
    413  #scrollWidth = null;
    414  #scrollX = null;
    415  #scrollY = null;
    416  #scrollMinX = null;
    417  #scrollMinY = null;
    418  #scrollMaxX = null;
    419  #scrollMaxY = null;
    420  #devicePixelRatio = null;
    421 
    422  set dimensions(dimensions) {
    423    if (dimensions.clientHeight != null) {
    424      this.#clientHeight = dimensions.clientHeight;
    425    }
    426    if (dimensions.clientWidth != null) {
    427      this.#clientWidth = dimensions.clientWidth;
    428    }
    429    if (dimensions.scrollHeight != null) {
    430      this.#scrollHeight = dimensions.scrollHeight;
    431    }
    432    if (dimensions.scrollWidth != null) {
    433      this.#scrollWidth = dimensions.scrollWidth;
    434    }
    435    if (dimensions.scrollX != null) {
    436      this.#scrollX = dimensions.scrollX;
    437    }
    438    if (dimensions.scrollY != null) {
    439      this.#scrollY = dimensions.scrollY;
    440    }
    441    if (dimensions.scrollMinX != null) {
    442      this.#scrollMinX = dimensions.scrollMinX;
    443    }
    444    if (dimensions.scrollMinY != null) {
    445      this.#scrollMinY = dimensions.scrollMinY;
    446    }
    447    if (dimensions.scrollMaxX != null) {
    448      this.#scrollMaxX = dimensions.scrollMaxX;
    449    }
    450    if (dimensions.scrollMaxY != null) {
    451      this.#scrollMaxY = dimensions.scrollMaxY;
    452    }
    453    if (dimensions.devicePixelRatio != null) {
    454      this.#devicePixelRatio = dimensions.devicePixelRatio;
    455    }
    456  }
    457 
    458  get dimensions() {
    459    return {
    460      clientHeight: this.clientHeight,
    461      clientWidth: this.clientWidth,
    462      scrollHeight: this.scrollHeight,
    463      scrollWidth: this.scrollWidth,
    464      scrollX: this.scrollX,
    465      scrollY: this.scrollY,
    466      pageScrollX: this.pageScrollX,
    467      pageScrollY: this.pageScrollY,
    468      scrollMinX: this.scrollMinX,
    469      scrollMinY: this.scrollMinY,
    470      scrollMaxX: this.scrollMaxX,
    471      scrollMaxY: this.scrollMaxY,
    472      devicePixelRatio: this.devicePixelRatio,
    473    };
    474  }
    475 
    476  get clientWidth() {
    477    return this.#clientWidth;
    478  }
    479 
    480  get clientHeight() {
    481    return this.#clientHeight;
    482  }
    483 
    484  get scrollWidth() {
    485    return this.#scrollWidth;
    486  }
    487 
    488  get scrollHeight() {
    489    return this.#scrollHeight;
    490  }
    491 
    492  get scrollX() {
    493    return this.#scrollX - this.scrollMinX;
    494  }
    495 
    496  get pageScrollX() {
    497    return this.#scrollX;
    498  }
    499 
    500  get scrollY() {
    501    return this.#scrollY - this.scrollMinY;
    502  }
    503 
    504  get pageScrollY() {
    505    return this.#scrollY;
    506  }
    507 
    508  get scrollMinX() {
    509    return this.#scrollMinX;
    510  }
    511 
    512  get scrollMinY() {
    513    return this.#scrollMinY;
    514  }
    515 
    516  get scrollMaxX() {
    517    return this.#scrollMaxX;
    518  }
    519 
    520  get scrollMaxY() {
    521    return this.#scrollMaxY;
    522  }
    523 
    524  get devicePixelRatio() {
    525    return this.#devicePixelRatio;
    526  }
    527 
    528  isInViewport(rect) {
    529    // eslint-disable-next-line no-shadow
    530    let { left, top, right, bottom } = rect;
    531 
    532    if (
    533      left > this.scrollX + this.clientWidth ||
    534      right < this.scrollX ||
    535      top > this.scrollY + this.clientHeight ||
    536      bottom < this.scrollY
    537    ) {
    538      return false;
    539    }
    540    return true;
    541  }
    542 
    543  reset() {
    544    this.#clientHeight = 0;
    545    this.#clientWidth = 0;
    546    this.#scrollHeight = 0;
    547    this.#scrollWidth = 0;
    548    this.#scrollX = 0;
    549    this.#scrollY = 0;
    550    this.#scrollMinX = 0;
    551    this.#scrollMinY = 0;
    552    this.#scrollMaxX = 0;
    553    this.#scrollMaxY = 0;
    554  }
    555 }