summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/scroll.js
blob: e740e5c8f6bed76f94b5ec41c37eaeb0fd95af70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
  /**
   * Scroll the document so that the element "elem" appears in the viewport.
   *
   * @param {DOMNode} elem
   *        The element that needs to appear in the viewport.
   * @param {Boolean} centered
   *        true if you want it centered, false if you want it to appear on the
   *        top of the viewport. It is true by default, and that is usually what
   *        you want.
   * @param {Boolean} smooth
   *        true if you want the scroll to happen smoothly, instead of instantly.
   *        It is false by default.
   */
  function scrollIntoViewIfNeeded(elem, centered = true, smooth = false) {
    const win = elem.ownerDocument.defaultView;
    const clientRect = elem.getBoundingClientRect();

    // The following are always from the {top, bottom}
    // of the viewport, to the {top, …} of the box.
    // Think of them as geometrical vectors, it helps.
    // The origin is at the top left.

    const topToBottom = clientRect.bottom;
    const bottomToTop = clientRect.top - win.innerHeight;
    // We allow one translation on the y axis.
    let yAllowed = true;

    // disable smooth scrolling when the user prefers reduced motion
    const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
    smooth = smooth && !reducedMotion;

    const options = { behavior: smooth ? "smooth" : "auto" };

    // Whatever `centered` is, the behavior is the same if the box is
    // (even partially) visible.
    if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
      win.scrollBy(
        Object.assign(
          { left: 0, top: topToBottom - elem.offsetHeight },
          options
        )
      );
      yAllowed = false;
    } else if (
      (bottomToTop < 0 || !centered) &&
      bottomToTop >= -elem.offsetHeight
    ) {
      win.scrollBy(
        Object.assign(
          { left: 0, top: bottomToTop + elem.offsetHeight },
          options
        )
      );

      yAllowed = false;
    }

    // If we want it centered, and the box is completely hidden,
    // then we center it explicitly.
    if (centered) {
      if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
        const x = win.scrollX;
        const y =
          win.scrollY +
          clientRect.top -
          (win.innerHeight - elem.offsetHeight) / 2;
        win.scroll(Object.assign({ left: x, top: y }, options));
      }
    }
  }

  function closestScrolledParent(node) {
    if (node == null) {
      return null;
    }

    if (node.scrollHeight > node.clientHeight) {
      return node;
    }

    return closestScrolledParent(node.parentNode);
  }

  /**
   * Scrolls the element into view if it is not visible.
   *
   * @param {DOMNode|undefined} element
   *        The item to be scrolled to.
   *
   * @param {Object|undefined} options
   *        An options object which can contain:
   *          - container: possible scrollable container. If it is not scrollable, we will
   *                       look it up.
   *          - alignTo:   "top" or "bottom" to indicate if we should scroll the element
   *                       to the top or the bottom of the scrollable container when the
   *                       element is off canvas.
   *          - center:    Indicate if we should scroll the element to the middle of the
   *                       scrollable container when the element is off canvas.
   */
  function scrollIntoView(element, options = {}) {
    if (!element) {
      return;
    }

    const { alignTo, center, container } = options;

    const { top, bottom } = element.getBoundingClientRect();
    const scrolledParent = closestScrolledParent(
      container || element.parentNode
    );
    const scrolledParentRect = scrolledParent
      ? scrolledParent.getBoundingClientRect()
      : null;
    const isVisible =
      !scrolledParent ||
      (top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom);

    if (isVisible) {
      return;
    }

    if (center) {
      element.scrollIntoView({ block: "center" });
      return;
    }

    const scrollToTop = alignTo
      ? alignTo === "top"
      : !scrolledParentRect || top < scrolledParentRect.top;
    element.scrollIntoView(scrollToTop);
  }

  // Exports from this module
  module.exports.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded;
  module.exports.scrollIntoView = scrollIntoView;
});