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