Source code

Revision control

Copy as Markdown

Other Tools

/* 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;
});