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 };