web-dev-qa-db-fra.com

Comment savoir que le défilement vers l'élément se fait en Javascript?

J'utilise la méthode Javascript Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

Existe-t-il un moyen de savoir quand le défilement est terminé? Disons qu'il y a eu une animation, ou j'ai défini {behavior: smooth}.

Je suppose que le défilement est asynchrone et je veux savoir s'il y a un mécanisme de rappel.

17
Sushant Gupta

Vous pouvez utiliser IntersectionObserver , vérifiez si l'élément .isIntersecting à IntersectionObserver fonction de rappel

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>
11
guest271314

Il n'y a pas d'événement scrollEnd, mais vous pouvez écouter l'événement scroll et vérifier s'il fait toujours défiler la fenêtre:

var scrollTimeout;
addEventListener('scroll', function(e) {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(function() {
        console.log('Scroll ended');
    }, 100);
});
6
niutech

je ne suis pas un expert en javascript mais je l'ai fait avec jQuery. j'espère que ça aide

$("#mybtn").click(function() {
    $('html, body').animate({
        scrollTop: $("div").offset().top
    }, 2000);
});

$( window ).scroll(function() {
  $("div").html("scrolling");
  if($(window).scrollTop() == $("div").offset().top) {
    $("div").html("Ended");
  }
})
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>
2
MajiD

Pour ce comportement "fluide", tous les spécifications disons que

Lorsqu'un agent utilisateur doit effectuer un défilement régulier d'une zone de boîte de défilement jusqu'à sa position, il doit mettre à jour la position de défilement de la zone d'une manière définie par l'agent utilisateur sur une durée définie par l'agent utilisateur .

(souligne le mien)

Donc non seulement il n'y a pas d'événement unique qui se déclenche une fois qu'il est terminé, mais nous ne pouvons même pas supposer un comportement stabilisé entre différents navigateurs.

Et en effet, Firefox actuel et Chrome diffèrent déjà dans leur comportement:

  • Firefox semble avoir une durée fixe, et quelle que soit la distance à parcourir, il le fera dans cette durée fixe (~ 500 ms)
  • Chrome d'autre part utilisera une vitesse, c'est-à-dire que la durée de l'opération variera en fonction de la distance à parcourir, avec une limite fixe de 3s.

Donc, cela disqualifie déjà toutes les solutions basées sur le délai d'attente pour ce problème.

Maintenant, l'une des réponses ici a proposé d'utiliser un MutationObserver, qui n'est pas une trop mauvaise solution, mais qui n'est pas trop portable, et ne prend pas les options inline et block en Compte.

Donc, le mieux est peut-être de vérifier régulièrement si nous avons arrêté le défilement. Pour ce faire de manière non invasive, nous pouvons démarrer une boucle alimentée requestAnimationFrame, afin que nos vérifications ne soient effectuées qu'une seule fois par trame.

Voici une telle implémentation, qui renverra une promesse qui sera résolue une fois l'opération de défilement terminée.
Remarque: Ce code manque un moyen de vérifier si l'opération a réussi, car si une autre opération de défilement se produit sur la page, toutes les opérations en cours sont annulées, mais je laisse cela comme un exercice pour le lecteur.

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>
0
Kaiido

Ces réponses ci-dessus laissent le gestionnaire d'événements en place même après le défilement (de sorte que si l'utilisateur défile, sa méthode continue d'être appelée). Ils ne vous informent pas non plus si aucun défilement n'est requis. Voici une réponse légèrement meilleure:

$("#mybtn").click(function() {
    $('html, body').animate({
        scrollTop: $("div").offset().top
    }, 2000);

    $("div").html("Scrolling...");

    callWhenScrollCompleted(() => {
        $("div").html("Scrolling is completed!");
    });
});

// Wait for scrolling to stop.
function callWhenScrollCompleted(callback, checkTimeout = 200, parentElement = $(window)) {
  const scrollTimeoutFunction = () => {
    // Scrolling is complete
    parentElement.off("scroll");
    callback();
  };
  let scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);

  parentElement.on("scroll", () => {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
  });
}
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>
0
Ryan Shillington