export default function preserveScroll (callback) { // preserve the actual scroll position const scrollTop = window.scrollY // if the scroll position is at the top, we don't need to preserve it, just call the callback if (scrollTop <= 0) { callback() return } // get a reference element at the center of the viewport to track if content is added above it const ref = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2) const refTop = ref ? ref.getBoundingClientRect().top + scrollTop : scrollTop // observe the document for changes in height const observer = new window.MutationObserver(() => { // request animation frame to ensure the DOM is updated window.requestAnimationFrame(() => { // we can't proceed if we couldn't find a traceable reference element if (!ref) { cleanup() return } // get the new position of the reference element along with the new scroll position const newRefTop = ref ? ref.getBoundingClientRect().top + window.scrollY : window.scrollY // has the reference element moved? const refMoved = newRefTop - refTop // if the reference element moved, we need to scroll to the new position if (refMoved > 0) { window.scrollTo({ // some browsers don't respond well to fractional scroll position, so we round up the new position to the nearest integer top: scrollTop + Math.ceil(refMoved), behavior: 'instant' }) } cleanup() }) }) const timeout = setTimeout(() => cleanup(), 1000) // fallback function cleanup () { clearTimeout(timeout) observer.disconnect() } observer.observe(document.body, { childList: true, subtree: true }) callback() }