tor-browser

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

scroll.mjs (4852B)


      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 /* eslint no-shadow: ["error", { "allow": ["top"] }] */
      6 
      7 /**
      8 * Scroll the document so that the element "elem" appears in the viewport.
      9 *
     10 * @param {DOMNode} elem
     11 *        The element that needs to appear in the viewport.
     12 * @param {boolean} centered
     13 *        true if you want it centered, false if you want it to appear on the
     14 *        top of the viewport. It is true by default, and that is usually what
     15 *        you want.
     16 * @param {boolean} smooth
     17 *        true if you want the scroll to happen smoothly, instead of instantly.
     18 *        It is false by default.
     19 */
     20 function scrollIntoViewIfNeeded(elem, centered = true, smooth = false) {
     21  const win = elem.ownerDocument.defaultView;
     22  const clientRect = elem.getBoundingClientRect();
     23 
     24  // The following are always from the {top, bottom}
     25  // of the viewport, to the {top, …} of the box.
     26  // Think of them as geometrical vectors, it helps.
     27  // The origin is at the top left.
     28 
     29  const topToBottom = clientRect.bottom;
     30  const bottomToTop = clientRect.top - win.innerHeight;
     31  // We allow one translation on the y axis.
     32  let yAllowed = true;
     33 
     34  // disable smooth scrolling when the user prefers reduced motion
     35  const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
     36  smooth = smooth && !reducedMotion;
     37 
     38  const options = { behavior: smooth ? "smooth" : "auto" };
     39 
     40  // Whatever `centered` is, the behavior is the same if the box is
     41  // (even partially) visible.
     42  if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
     43    win.scrollBy(
     44      Object.assign({ left: 0, top: topToBottom - elem.offsetHeight }, options)
     45    );
     46    yAllowed = false;
     47  } else if (
     48    (bottomToTop < 0 || !centered) &&
     49    bottomToTop >= -elem.offsetHeight
     50  ) {
     51    win.scrollBy(
     52      Object.assign({ left: 0, top: bottomToTop + elem.offsetHeight }, options)
     53    );
     54 
     55    yAllowed = false;
     56  }
     57 
     58  // If we want it centered, and the box is completely hidden,
     59  // then we center it explicitly.
     60  if (centered) {
     61    if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
     62      const x = win.scrollX;
     63      const y =
     64        win.scrollY +
     65        clientRect.top -
     66        (win.innerHeight - elem.offsetHeight) / 2;
     67      win.scroll(Object.assign({ left: x, top: y }, options));
     68    }
     69  }
     70 }
     71 
     72 function closestScrolledParent(node) {
     73  if (node == null) {
     74    return null;
     75  }
     76 
     77  const window = node.ownerDocument?.defaultView || node.defaultView;
     78  // Typically ignore Document when reaching the top-most element
     79  const isElement = node instanceof window.HTMLElement;
     80 
     81  // Ensure that the scrolled parent can actually scroll.
     82  // In the debugger's scope panel, the "div.accordion" has scrollable content
     83  // but "div.secondary-panes" is the parent element with overflow:auto
     84  const overflowY = isElement ? window.getComputedStyle(node).overflowY : null;
     85  const isScrollable = overflowY !== "visible" && overflowY !== "hidden";
     86 
     87  if (isScrollable && node.scrollHeight > node.clientHeight) {
     88    return node;
     89  }
     90 
     91  return closestScrolledParent(node.parentNode);
     92 }
     93 
     94 /**
     95 * Scrolls the element into view if it is not visible.
     96 *
     97 * @param {DOMNode|undefined} element
     98 *        The item to be scrolled to.
     99 *
    100 * @param {object | undefined} options
    101 *        An options object which can contain:
    102 *          - container: possible scrollable container. If it is not scrollable, we will
    103 *                       look it up.
    104 *          - alignTo:   "top" or "bottom" to indicate if we should scroll the element
    105 *                       to the top or the bottom of the scrollable container when the
    106 *                       element is off canvas.
    107 *          - center:    Indicate if we should scroll the element to the middle of the
    108 *                       scrollable container when the element is off canvas.
    109 */
    110 function scrollIntoView(element, options = {}) {
    111  if (!element) {
    112    return;
    113  }
    114 
    115  const { alignTo, center, container } = options;
    116 
    117  const { top, bottom } = element.getBoundingClientRect();
    118  const scrolledParent = closestScrolledParent(container || element.parentNode);
    119  const scrolledParentRect = scrolledParent
    120    ? scrolledParent.getBoundingClientRect()
    121    : null;
    122  const isVisible =
    123    !scrolledParent ||
    124    (top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom);
    125 
    126  if (isVisible) {
    127    return;
    128  }
    129 
    130  if (center) {
    131    element.scrollIntoView({ block: "center" });
    132    return;
    133  }
    134 
    135  const scrollToTop = alignTo
    136    ? alignTo === "top"
    137    : !scrolledParentRect || top < scrolledParentRect.top;
    138  element.scrollIntoView(scrollToTop);
    139 }
    140 
    141 // Exports from this module
    142 export { scrollIntoViewIfNeeded, scrollIntoView };