如何知道滚动到元素是在 Javascript 中完成的?

Posted

技术标签:

【中文标题】如何知道滚动到元素是在 Javascript 中完成的?【英文标题】:How to know scroll to element is done in Javascript? 【发布时间】:2021-11-25 10:03:27 【问题描述】:

我正在使用javascript方法Element.scrollIntoView()https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

有什么方法可以让我知道卷轴何时结束。说有动画,或者我设置了behavior: smooth

我假设滚动是异步的,我想知道它是否有类似回调的机制。

【问题讨论】:

VanillaJS 中的工作解决方案,没有 setTimeout ***.com/a/52292810/3759551 【参考方案1】:

你可以使用IntersectionObserver,在IntersectionObserver回调函数中检查元素.isIntersecting

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>

【讨论】:

@SushantGupta "IntersectionObserver is not supported in Safari" .scrollIntoView() is not supported at Safari or based on link to MDN at Question. scrollIntoView 在 Safari 中为我工作。只是不支持behavior: "smooth"。 caniuse.com/#search=scrollintoview 另外,在文档中 scrollIntoViewOptions 在 Safari 中不受支持,但 scrollIntoView 受支持。 很酷的功能。但是在您的代码中同时使用 boxelement 变量。它的东西眩晕.. 我猜这只有在滚动前“box”元素不可见时才会起作用。但它可能一直可见,IntersectionObserver 将无法检测到它。【参考方案2】:

没有scrollEnd事件,但是你可以监听scroll事件,看看它是否还在滚动窗口:

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

【讨论】:

这对我来说非常有效。我的scrollIntoView() 触发了一个不相关的滚动功能,所以我在scrollIntoView 之前删除了侦听器,但我需要一种方法来知道滚动何时完成以重新添加侦听器。好消息是在这里添加带有命名函数的事件监听器可以防止它被重复添加。【参考方案3】:

我不是 javascript 方面的专家,但我是用 jQuery 制作的。希望对你有帮助

$("#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>

【讨论】:

【参考方案4】:

即使在滚动完成后,上面的这些答案也会保留事件处理程序(因此,如果用户滚动,他们的方法会继续被调用)。如果不需要滚动,他们也不会通知您。这是一个稍微好一点的答案:

$("#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>

【讨论】:

【参考方案5】:

对于这种“流畅”的行为,the specs 说的都是

当用户代理要将滚动框平滑滚动到某个位置时,它必须以用户代理定义的方式用户上更新框的滚动位置- 代理定义的时间量

(强调我的)

因此,不仅没有单个事件在完成后会触发,而且我们甚至无法假设不同浏览器之间的任何稳定行为。

事实上,当前的 Firefox 和 Chrome 在行为上已经有所不同:

Firefox 似乎设置了固定的持续时间,无论滚动的距离是多少,它都会在这个固定的持续时间(~500ms)内完成 另一方面,Chrome 会使用速度,即操作的持续时间会根据滚动的距离而有所不同,硬限制为 3 秒。

因此,这已经取消了针对此问题的所有基于超时的解决方案。

现在,这里的答案之一是建议使用 IntersectionObserver,这不是一个太糟糕的解决方案,但它不太便携,并且没有考虑到 inlineblock 选项。

所以实际上最好的办法是定期检查我们是否停止滚动。要以非侵入性方式执行此操作,我们可以启动 requestAnimationFrame 供电循环,这样我们的检查每帧只执行一次。

这里有一个这样的实现,它将返回一个 Promise,一旦滚动操作完成,该 Promise 将得到解决。注意:此代码缺少检查操作是否成功的方法,因为如果页面上发生了另一个滚动操作,所有当前的滚动操作都被取消了,但我会让它作为读者的练习。

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>

【讨论】:

【参考方案6】:

使用 rxjs 对我有用的解决方案

语言:打字稿

scrollToElementRef(
    element: HTMLElement,
    options?: ScrollIntoViewOptions,
    emitFinish = false,
  ): void | Promise<boolean> 
    element.scrollIntoView(options);
    if (emitFinish) 
      return fromEvent(window, 'scroll')
        .pipe(debounceTime(100), first(), mapTo(true)).toPromise();
    
  

用法:

const element = document.getElementById('ELEM_ID');
scrollToElementRef(elment, behavior: 'smooth', true).then(() => 
  // scroll finished do something
)

【讨论】:

【参考方案7】:

我偶然发现了这个问题,因为我想在滚动完成后关注特定输入(这样我才能保持平滑滚动)。

如果你和我有同样的用例,你实际上不需要等待滚动完成来聚焦你的输入,你可以简单地禁用焦点滚动

这是如何完成的:

window.scrollTo( top: 0, behavior: "smooth" );
myInput.focus( preventScroll: true );

cf:https://github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932

顺便说一句,在 CSSWG GitHub 中讨论了这个特定问题(在执行操作之前等待滚动完成):https://github.com/w3c/csswg-drafts/issues/3744

【讨论】:

【参考方案8】:

我最近需要 element.scrollIntoView() 的回调方法。因此尝试使用 Krzysztof Podlaski 的答案。 但我不能按原样使用它。我稍微修改了一下。

import  fromEvent, lastValueFrom  from 'rxjs';
import  debounceTime, first, mapTo  from 'rxjs/operators';

/**
 * This function allows to get a callback for the scrolling end
 */
const scrollToElementRef = (parentEle, childEle, options) => 
  // If parentEle.scrollTop is 0, the parentEle element does not emit 'scroll' event. So below is needed.
  if (parentEle.scrollTop === 0) return Promise.resolve(1);
  childEle.scrollIntoView(options);
  return lastValueFrom(
    fromEvent(parentEle, 'scroll').pipe(
      debounceTime(100),
      first(),
      mapTo(true)
    )
  );
;

如何使用

scrollToElementRef(
  scrollableContainerEle, 
  childrenEle, 
  
    behavior: 'smooth',
    block: 'end',
    inline: 'nearest',
  
).then(() => 
  // Do whatever you want ;)
);

【讨论】:

以上是关于如何知道滚动到元素是在 Javascript 中完成的?的主要内容,如果未能解决你的问题,请参考以下文章

平滑 JavaScript/jQuery 滚动到元素

平滑 JavaScript/jQuery 滚动到元素

Jquery / Javascript滚动到只有类元素的页面中的div

使用纯 Javascript 滚动到可滚动 DIV 内的元素

使用纯 Javascript 滚动到可滚动 DIV 内的元素

如何使用 Web 驱动程序采样器(J 表)和 javascript 和 selenium wedriver 向上和向下滚动到任何元素