我正在使用 Javascript 方法Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
有什么方法可以让我知道卷轴何时结束。假设有动画,或者我设置了{behavior: smooth}
.
我假设滚动是异步的,并且想知道是否有任何类似回调的机制。
我正在使用 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);
});
您可以使用,检查回调函数中的IntersectionObserver
元素.isIntersecting
IntersectionObserver
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>
对于这种“流畅”的行为,所有规格都说是
当用户代理要执行滚动框的平滑滚动到定位时,它必须在用户代理定义的时间内以用户代理定义的方式更新框的滚动位置。
(强调我的)
因此,不仅没有单个事件一旦完成就会触发,而且我们甚至不能假设不同浏览器之间的任何稳定行为。
事实上,当前的 Firefox 和 Chrome 在行为上已经有所不同:
因此,这已经取消了针对此问题的所有基于超时的解决方案。
现在,这里的答案之一是建议使用 IntersectionObserver,这不是一个太糟糕的解决方案,但它不太便携,并且没有考虑inline
andblock
选项。
所以最好的方法实际上可能是定期检查我们是否停止滚动。要以非侵入性方式执行此操作,我们可以启动一个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>
使用 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>
我偶然发现了这个问题,因为我想在滚动完成后关注一个特定的输入(这样我就可以保持平滑滚动)。
如果你和我有同样的用例,你实际上不需要等待滚动完成来关注你的输入,你可以简单地禁用 focus 的滚动。
这是如何完成的:
window.scrollTo({ top: 0, behavior: "smooth" });
myInput.focus({ preventScroll: true });
参考:https ://github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932
顺便说一句,这里的 CSSWG GitHub 讨论了这个特定问题(在执行操作之前等待滚动完成):https ://github.com/w3c/csswg-drafts/issues/3744
我最近需要 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 ;)
});