Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to know scroll to element is done in Javascript?

I am using Javascript method Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

Is there any way I can get to know when the scroll is over. Say there was an animation, or I have set {behavior: smooth}.

I am assuming scrolling is async and want to know if there is any callback like mechanism to it.

like image 790
Sushant Gupta Avatar asked Oct 17 '17 17:10

Sushant Gupta


People also ask

How do you know if an element is visible after scrolling?

Summary. Use the getBoundingClientRect() method to get the size of the element and its relative position to the viewport. Compare the position of the element with the viewport height and width to check if the element is visible in the viewport or not.

How do I check if JavaScript is scrolling?

To detect if a user is scrolling with JavaScript, we can watch for scroll events by adding event handlers. to add the userHasScrolled variable. Then we set the window. onscroll property to a function that runs when we scroll.

How do I know which element is scrolling?

Hit F12 to open it and check the dom elements. You'll be able to find it.

What is the JavaScript method used to scroll to a particular element?

The scrollTo method: The scrollTo() is used to scroll to the specified element in the browser.


3 Answers

There is no scrollEnd event, but you can listen for the scroll event and check if it is still scrolling the window:

var scrollTimeout;
addEventListener('scroll', function(e) {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(function() {
        console.log('Scroll ended');
    }, 100);
});
like image 128
niutech Avatar answered Oct 22 '22 05:10

niutech


For this "smooth" behavior, all the specs say is

When a user agent is to perform a smooth scroll of a scrolling box box to position, it must update the scroll position of box in a user-agent-defined fashion over a user-agent-defined amount of time.

(emphasis mine)

So not only is there no single event that will fire once it's completed, but we can't even assume any stabilized behavior between different browsers.

And indeed, current Firefox and Chrome already differ in their behavior:

  • Firefox seems to have a fixed duration set, and whatever the distance to scroll is, it will do it in this fixed duration ( ~500ms )
  • Chrome on the other hand will use a speed, that is, the duration of the operation will vary based on the distance to scroll, with an hard-limit of 3s.

So this already disqualifies all the timeout based solutions for this problem.

Now, one of the answers here has proposed to use an IntersectionObserver, which is not a too bad solution, but which is not too portable, and doesn't take the inline and block options into account.

So the best might actually be to check regularly if we did stop scrolling. To do this in a non-invasive way, we can start an requestAnimationFrame powered loop, so that our checks are performed only once per frame.

Here one such implementation, which will return a Promise that will get resolved once the scroll operation has finished.
Note: This code misses a way to check if the operation succeeded, since if an other scroll operation happens on the page, all current ones are cancelled, but I'll leave this as an exercise for the reader.

const buttons = [ ...document.querySelectorAll( 'button' ) ];

document.addEventListener( 'click', ({ target }) => {
  // handle delegated event
  target = target.closest('button');
  if( !target ) { return; }
  // find where to go next
  const next_index =  (buttons.indexOf(target) + 1) % buttons.length;
  const next_btn = buttons[next_index];
  const block_type = target.dataset.block;

  // make it red
  document.body.classList.add( 'scrolling' );
  
  smoothScroll( next_btn, { block: block_type })
    .then( () => {
      // remove the red
      document.body.classList.remove( 'scrolling' );
    } )
});


/* 
 *
 * Promised based scrollIntoView( { behavior: 'smooth' } )
 * @param { Element } elem
 **  ::An Element on which we'll call scrollIntoView
 * @param { object } [options]
 **  ::An optional scrollIntoViewOptions dictionary
 * @return { Promise } (void)
 **  ::Resolves when the scrolling ends
 *
 */
function smoothScroll( elem, options ) {
  return new Promise( (resolve) => {
    if( !( elem instanceof Element ) ) {
      throw new TypeError( 'Argument 1 must be an Element' );
    }
    let same = 0; // a counter
    let lastPos = null; // last known Y position
    // pass the user defined options along with our default
    const scrollOptions = Object.assign( { behavior: 'smooth' }, options );

    // let's begin
    elem.scrollIntoView( scrollOptions );
    requestAnimationFrame( check );
    
    // this function will be called every painting frame
    // for the duration of the smooth scroll operation
    function check() {
      // check our current position
      const newPos = elem.getBoundingClientRect().top;
      
      if( newPos === lastPos ) { // same as previous
        if(same ++ > 2) { // if it's more than two frames
/* @todo: verify it succeeded
 * if(isAtCorrectPosition(elem, options) {
 *   resolve();
 * } else {
 *   reject();
 * }
 * return;
 */
          return resolve(); // we've come to an halt
        }
      }
      else {
        same = 0; // reset our counter
        lastPos = newPos; // remember our current position
      }
      // check again next painting frame
      requestAnimationFrame(check);
    }
  });
}
p {
  height: 400vh;
  width: 5px;
  background: repeat 0 0 / 5px 10px
    linear-gradient(to bottom, black 50%, white 50%);
}
body.scrolling {
  background: red;
}
<button data-block="center">scroll to next button <code>block:center</code></button>
<p></p>
<button data-block="start">scroll to next button <code>block:start</code></button>
<p></p>
<button data-block="nearest">scroll to next button <code>block:nearest</code></button>
<p></p>
<button>scroll to top</button>
like image 26
Kaiido Avatar answered Oct 22 '22 06:10

Kaiido


You can use IntersectionObserver, check if element .isIntersecting at IntersectionObserver callback function

const element = document.getElementById("box");

const intersectionObserver = new IntersectionObserver((entries) => {
  let [entry] = entries;
  if (entry.isIntersecting) {
    setTimeout(() => alert(`${entry.target.id} is visible`), 100)
  }
});
// start observing
intersectionObserver.observe(box);

element.scrollIntoView({behavior: "smooth"});
body {
  height: calc(100vh * 2);
}

#box {
  position: relative;
  top:500px;
}
<div id="box">
box
</div>
like image 24
guest271314 Avatar answered Oct 22 '22 07:10

guest271314