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



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

有什么办法可以知道卷轴何时结束。假设有动画,或者我已经设置了{behavior: smooth}.

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

没有scrollEnd事件,但您可以侦听scroll事件并检查它是否仍在滚动窗口:

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

2022 年更新:

CSS规范最近包括了 overscroll 和 scrollend 提案,该提案添加了一些 CSS overscroll 属性,更重要的是,对我们来说,这是一个scrollendevent.
Browsers 仍在努力实现它。(它已经在Chromium 的 Web 平台实验标志下可用。
我们可以通过简单地查找来检测它

if (window.onscrollend !== undefined) {
// we have a scrollend event
}

在等待到处实现的同时,如果你想构建一个 polyfill,这个答案的其余部分仍然很有用:


对于这种"平滑"行为,所有规范都说[说]是

当用户代理要执行滚动框到位置的

平滑滚动时,它必须在用户代理定义的时间内用户代理定义的方式更新框的滚动位置。

(强调我的)

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

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

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

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

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

因此,最好的方法可能是定期检查我们是否确实停止滚动。 为了以非侵入性的方式做到这一点,我们可以启动一个requestAnimationFrame的动力循环,这样我们的检查每帧只执行一次。

这里有一个这样的实现,它将返回一个 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>

您可以使用IntersectionObserver,检查元素是否.isIntersectingIntersectionObserver回调函数

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(element);
element.scrollIntoView({behavior: "smooth"});
body {
height: calc(100vh * 2);
}
#box {
position: relative;
top:500px;
}
<div id="box">
box
</div>

我偶然发现了这个问题,因为我想在滚动完成后专注于特定的输入(以便保持平滑滚动)。

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

这是它是如何完成的:

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

适用于我的解决方案 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
})

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

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

我不是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>

接受的答案很棒,但由于它的冗长,我几乎没有使用它。 这是一个更简单的香草版本,应该不言自明:

scrollTarget.scrollIntoView({
behavior: "smooth",
block: "center",
});
let lastPos = null;
requestAnimationFrame(checkPos);
function checkPos() {
const newPos = scrollTarget.getBoundingClientRect().top;
if (newPos === lastPos) {
console.log('scroll finished on', scrollTarget);
} else {
lastPos = newPos;
requestAnimationFrame(checkPos);
}
}

我省略了OP担心英国皇家空军会在没有滚动变化的情况下快速连续发射两次的检查; 也许这是一种合理的恐惧,但我没有遇到这个问题。

我最近需要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 ;)
});

如果有人正在寻找一种通过指令识别Angular 中的 scrollEnd 事件的方法:

/**
* As soon as the current scroll animation ends
* (triggered by scrollElementIntoView({behavior: 'smooth'})),
* this method resolves the returned Promise.
*/
@Directive({
selector : '[scrollEndRecognizer]'
})
export class ScrollEndDirective {
@Output() scrollEnd: EventEmitter<void> = new EventEmitter();
private scrollTimeoutId: number;
//*****************************************************************************
//  Events
//****************************************************************************/
@HostListener('scroll', [])
public emitScrollEndEvent() {
// On each new scroll event, clear the timeout.
window.clearTimeout(this.scrollTimeoutId);
// Only after scrolling has ended, the timeout executes and emits an event.
this.scrollTimeoutId = window.setTimeout(() => {
this.scrollEnd.emit();
this.scrollTimeoutId = null;
}, 100);
}
/////////////////////////////////////////////////////////////////////////////*/
//  END Events
/////////////////////////////////////////////////////////////////////////////*/
}

相关内容

  • 没有找到相关文章

最新更新