How to know scroll to element is done in Javascript? - 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.

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);
});

2022 Update:
The CSS specs recently included the overscroll and scrollend proposal, this proposal adds a few CSS overscroll attributes, and more importantly to us, a scrollend event.
Browsers are still working on implementing it. (It's already available in Chromium under the Web Platforms Experiments flag.)
We can feature-detect it by simply looking for
if (window.onscrollend !== undefined) {
// we have a scrollend event
}
While waiting for implementations everywhere, the remaining of this answer is still useful if you want to build a polyfill:
For this "smooth" behavior, all the specs say[said] 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>

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

I stumbled across this question as I wanted to focus a particular input after the scrolling is done (so that I keep the smooth scrolling).
If you have the same usecase as me, you don't actually need to wait for the scroll to be finished to focus your input, you can simply disable the scrolling of focus.
Here is how it's done:
window.scrollTo({ top: 0, behavior: "smooth" });
myInput.focus({ preventScroll: true });
cf: https://github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932
Btw this particular issue (of waiting for scroll to finish before executing an action) is discussed in CSSWG GitHub here: https://github.com/w3c/csswg-drafts/issues/3744

Solution that work for me with rxjs
lang: Typescript
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();
}
}
Usage:
const element = document.getElementById('ELEM_ID');
scrollToElementRef(elment, {behavior: 'smooth'}, true).then(() => {
// scroll finished do something
})

These answers above leave the event handler in place even after the scrolling is done (so that if the user scrolls, their method keeps getting called). They also don't notify you if there's no scrolling required. Here's a slightly better answer:
$("#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>

i'm not an expert in javascript but i made this with jQuery. i hope it helps
$("#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>

I recently needed callback method of element.scrollIntoView(). So tried to use the Krzysztof Podlaski's answer.
But I could not use it as is. I modified a little.
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)
)
);
};
How to use
scrollToElementRef(
scrollableContainerEle,
childrenEle,
{
behavior: 'smooth',
block: 'end',
inline: 'nearest',
}
).then(() => {
// Do whatever you want ;)
});

Related

Trigger rendering of all `content-visibility: auto` elements BEFORE a user scrolls to them without blocking main thread for >50ms

I want to have a fast page-load but also not have the user see a delay before content is rendered while scrolling down the page.
I have a large html page with many <dl> elements.
To speed up page load, I have set content-visibility: auto in css. See https://web.dev/content-visibility/
dl {
content-visibility: auto;
contain-intrinsic-size: 1000px;
}
Due to the complexity of the contents of the <dl>s there is a noticeable delay when a user scrolls while the <dl>s are rendered as they come into the viewport.
Thus, soon after page-load I want to render all the offscreen <dl> straight away (before a user scrolls to them) but in such a way that it does not block the main thread and scrolling remains responsive.
So, I want to set content-visibility: visible on the <dl>s starting from the top one, and not blocking the main thread (for more than say 50ms). So, maybe allowing user interaction after rendering each <dl>.
So, I need a version of the below, that doesn't block the main thread:
document.querySelectorAll('dl').forEach(function(dlElement, currentIndex, listObj) { dlElement.style['content-visibility'] = 'visible' });
My use case: My page is of math notes, which I want all on one page to reduce friction. I use katex which (for now, before we can use mathml on chrome) produces very large and complex html, which even server-side rendered still takes a lot of time for layout and rendering on the browser.
Rather than leave this unanswered, let me paste my (unperfected) code I have been testing for the last few weeks.
// --------- Shim requestIdleCallback if not supported in browser ----------------------------------
window.requestIdleCallback =
window.requestIdleCallback ||
function (cb) {
var start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}
window.cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
}
// Global
let isRequestIdleCallbackScheduled = false;
let nodeToRemove = null;
let isVisualUpdateScheduled = false;
let totalDlElementsLeftToShow = 0;
function startIfNeeded() {
totalDlElementsLeftToShow = document.querySelectorAll('dl:not([style*="content-visibility: visible;"]), .ra:not([style*="content-visibility: visible;"]').length;
if (totalDlElementsLeftToShow > 0) {
console.log('Not a mobile - Let\'s make things visible when we are next idle');
scheduleVisibilityChanges();
}
else {
console.log('Apparently, all visible');
}
}
function scheduleVisibilityChanges() {
// Only schedule the rIC if one has not already been set.
if (isRequestIdleCallbackScheduled) {
//console.log('returning because idle callback scheduled');
startIfNeeded();
}
isRequestIdleCallbackScheduled = true;
//console.log('scheduling visibility changes when next idle or in 30 seconds at the latest');
requestIdleCallback(processHiddenElements, { timeout: 30000 });
}
function processHiddenElements(deadline) {
// Since our operation takes a while, we only want to go ahead if we have at least 50ms.
while (deadline.timeRemaining() > 49 && totalDlElementsLeftToShow > 0) {
// console.log('time remaining is ', deadline.timeRemaining(), '- scheduling next visual update');
// Don't set content-visibility immediately wait for the next
// requestAnimationFrame callback.
scheduleVisualUpdateIfNeeded();
}
// console.log('Deadline reached, will check again the next time the user is idle if there are more events still to send');
if (totalDlElementsLeftToShow > 0) {
requestIdleCallback(processHiddenElements, { timeout: 30000 });
}
}
function scheduleVisualUpdateIfNeeded() {
if (isVisualUpdateScheduled) {
// console.log('returning - visual update already scheduled')
return;
};
isVisualUpdateScheduled = true;
// console.log('requesting animation frame');
requestAnimationFrame(setContentToVisible);
}
function setContentToVisible() {
// console.log('changing visibility of element ');
let completeHiddenNodeList = document.querySelectorAll('dl:not([style*="content-visibility: visible;"]), .ra:not([style*="content-visibility: visible;"]');
// We chunk the layout changes
let i;
let numberToChunk = 20;
if (completeHiddenNodeList.length < 20) {
numberToChunk = completeHiddenNodeList.length
}
for (i = 0; i < numberToChunk; ++i) {
completeHiddenNodeList[i].style.contentVisibility = 'visible';
}
isVisualUpdateScheduled = false;
isRequestIdleCallbackScheduled = false;
totalDlElementsLeftToShow = totalDlElementsLeftToShow - numberToChunk;
}
if (!navigator.userAgentData.mobile) {
startIfNeeded();
}

How to remove scroll event listener?

I am trying to remove scroll event listener when I scroll to some element. What I am trying to do is call a click event when some elements are in a viewport. The problem is that the click event keeps calling all the time or after first call not at all. (Sorry - difficult to explain) and I would like to remove the scroll event to stop calling the click function.
My code:
window.addEventListener('scroll', () => {
window.onscroll = slideMenu;
// offsetTop - the distance of the current element relative to the top;
if (window.scrollY > elementTarget.offsetTop) {
const scrolledPx = (window.scrollY - elementTarget.offsetTop);
// going forward one step
if (scrolledPx < viewportHeight) {
// console.log('section one');
const link = document.getElementById('2');
if (link.stopclik === undefined) {
link.click();
link.stopclik = true;
}
}
// SECOND STEP
if (viewportHeight < scrolledPx && (viewportHeight * 2) > scrolledPx) {
console.log('section two');
// Initial state
let scrollPos = 0;
window.addEventListener('scroll', () => {
if ((document.body.getBoundingClientRect()).top > scrollPos) { // UP
const link1 = document.getElementById('1');
link1.stopclik = undefined;
if (link1.stopclik === undefined) {
link1.click();
link1.stopclik = true;
}
} else {
console.log('down');
}
// saves the new position for iteration.
scrollPos = (document.body.getBoundingClientRect()).top;
});
}
if ((viewportHeight * 2) < scrolledPx && (viewportHeight * 3) > scrolledPx) {
console.log('section three');
}
const moveInPercent = scrolledPx / base;
const move = -1 * (moveInPercent);
innerWrapper.style.transform = `translate(${move}%)`;
}
});
You can only remove event listeners on external functions. You cannot remove event listeners on anonymous functions, like you have used.
Replace this code
window.addEventListener('scroll', () => { ... };
and do this instead
window.addEventListener('scroll', someFunction);
Then move your function logic into the function
function someFunction() {
// add logic here
}
You can then remove the click listener when some condition is met i.e. when the element is in the viewport
window.removeEventListener('scroll', someFunction);
Instead of listening to scroll event you should try using Intersection Observer (IO) Listening to scroll event and calculating the position of elements on each scroll can be bad for performance. With IO you can use a callback function whenever two elements on the page are intersecting with each other or intersecting with the viewport.
To use IO you first have to specify the options for IO. Since you want to check if your element is in view, leave the root element out.
let options = {
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
Then you specify which elements you want to watch:
let target = slideMenu; //document.querySelector('#oneElement') or document.querySelectorAll('.multiple-elements')
observer.observe(target); // if you have multiple elements, loop through them to add observer
Lastly you have to define what should happen once the element is in the viewport:
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
});
};
You can also unobserve an element if you don't need the observer anymore.
Check this polyfill from w3c to support older browsers.
Here is my scenario/code, call removeEventListener as return() in the useEffect hook.
const detectPageScroll = () => {
if (window.pageYOffset > YOFFSET && showDrawer) {
// do something
}
};
React.useEffect(() => {
if (showDrawer) {
window.addEventListener("scroll", detectPageScroll);
}
return () => {
window.removeEventListener("scroll", detectPageScroll);
};
}, [showDrawer]);

mouse wheel firing more than one time

How can I make the mousewheel function fire one time only instead of hunderds of times(multiple of times) when the user scrolls.
Here is my work so far,
$(window).bind('mousewheel', function(event) {
console.log("fire")
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div></div>
<image src="https://cdn.dribbble.com/users/77760/screenshots/2042501/attachments/363345/potato-4.jpg"></image>
I find logic of detecting ending of scroll from here if scroll not heppend for 250ms then it will take as end of scroll
var i = 0;
$(window).bind('mousewheel', function(event) {
if(i==0){
console.log("fist time")
i++;
}
clearTimeout($.data(this, 'scrollTimer'));
$.data(this, 'scrollTimer', setTimeout(function() {
// do something
console.log("Haven't scrolled in 250ms!");
i = 0;
}, 250));
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div></div>
<image src="https://cdn.dribbble.com/users/77760/screenshots/2042501/attachments/363345/potato-4.jpg"></image>
You are going to want to throttle the scroll event to make sure it doesn't keep firing. Here is an example throttle function:
const throttle = (func, limit) => {
let inThrottle
return function() {
const args = arguments
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
You're only allowing the scroll event to fire every x milliseconds. Where limit sets the time in milliseconds to wait before allowing the event to fire again. There are also libraries that provide this sort of functionality such as lodash and RxJs
A useful link: http://underscorejs.org/#throttle
Throttle function taken from: https://codeburst.io/throttling-and-debouncing-in-javascript-b01cad5c8edf
You could set the eventListener and remove it as soon as its gets triggered, something like this:
$(window).on("mousewheel", function(e){
console.log("only alerting once");
$(window).unbind("mousewheel");
});
You could define a variable to hold the value if the user has scrolled or not, set it to false, then once the user scrolls, set it to true.
Also note, As of jQuery 3.0, .bind() has been deprecated. It is best practice now to use .on() instead.
.on() has been the go to method for attaching event handlers to a document since jquery version 1.7
var scrolled = false;
$(window).on('mousewheel', function(event) {
if(scrolled === false) {
console.log('fire');
scrolled = true;
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div></div>
<image src="https://cdn.dribbble.com/users/77760/screenshots/2042501/attachments/363345/potato-4.jpg"></image>

Can I detect if an arbitrary CSS transition has started

In my close function I want to do all my DOM clean-up stuff after css transitions have finished running. But there might not be any transitions running/might be multi-stage ones - (maintaining the stylesheets is out of my hands).
How would I go about writing a function something like the following
function close () {
myEl.removeClass('open');
if (animation is running/about to be run) {
// wait for transition to end, then recursively check to see if another
// one has started, wait for that ...
// then
cleanUpDOM();
} else {
cleanUpDOM();
}
}
My thoughts so far are to wrap the initial check in a timeout/requestAnimationFrame in order to give the animation a chance to start then checking to see if it's running. Unfortunately, without a transitionstart event I have no idea how to check if a transition has begun.
edit Answers recommending jquery are irrelevant as jquery animations are javascript animations, not CSS transitions
About transitionStart and transitionEnd events:
The transition can't starts from nowhere. Usually transition starts after some event, where you change the state of DOM element by changing styles by class or something else. So you know when transition starts because you start it in your code.
During the transition user I/O don't blocks, so transition is asynchronous and then transition will end you don't know right. So you needs transitionEnd event to do something then transition has finished in javascript.
About transitionEnd event:
Just look the jsfiddle
Here's my solution so far - a bit hacky and only works when which element might transition is known, and doesn't work with transition-property: all... but it's a promising start
function toCamelStyleProp (str) {
return str.replace(/(?:\-)([a-z])/gi, function ($0, $1) {
return $1.toUpperCase();
});
}
function toHyphenatedStyleProp (str) {
return str.replace(/([A-Z])/g, function (str,m1) {
return '-' + m1.toLowerCase();
}).replace(/^ms-/,'-ms-');
}
function getPrefixedStyleProp (prop) {
prop = toCamelStyleProp(prop);
prop = Modernizr.prefixed(prop);
return toHyphenatedStyleProp(prop);
}
function getStyleProperty (el, prop) {
return getComputedStyle(el,null).getPropertyValue(getPrefixedStyleProp(prop));
}
function doAfterTransition ($wrapper, cssClass, mode, $transitioningEl, callback) {
$transitioningEl = $transitioningEl || $wrapper;
var transitioningEl = $transitioningEl[0],
duration = +getStyleProperty(transitioningEl, 'transition-duration').replace(/[^\.\d]/g, ''),
transitioners = getStyleProperty(transitioningEl, 'transition-property').split(' '),
initialState = [],
changedState = [],
i,
callbackHasRun = false,
//makes sure callback doesn't get called twice by accident
singletonCallback = function () {
if (!callbackHasRun) {
callbackHasRun = true;
callback();
}
};
// if no transition defined just call the callback
if (duration === 0) {
$wrapper[mode + 'Class'](cssClass);
callback();
return;
}
for (i = transitioners.length - 1;i>=0;i--) {
initialState.unshift(getStyleProperty(transitioningEl, transitioners[i]));
}
$wrapper[mode + 'Class'](cssClass);
setTimeout(function () {
for (i = transitioners.length - 1;i>=0;i--) {
changedState.unshift(getStyleProperty(transitioningEl, transitioners[i]));
}
for (i = transitioners.length - 1;i>=0;i--) {
if (changedState[i] !== initialState[i]) {
$transitioningEl.transitionEnd(singletonCallback);
// failsafe in case the transitionEnd event doesn't fire
setTimeout(singletonCallback, duration * 1000);
return;
}
}
singletonCallback();
}, 20);
}
There is no way (that I know of) to detect if a transition is currently working in the background without knowing the element that is being transitioned.
However, if you can move away from transition to key frame animations, then you'd have the so needed event - animationStart and animationEnd and then it will be easy to figure out if there are running animations.
If you're planning to make css transition, you can check out jQuery Transit Plugin http://ricostacruz.com/jquery.transit/
Very powerfull and useful, you can get transform x value with. css('x') for example.
Have you tried the JQuery pseudo ":animated"?
if( $(elem).is(':animated') ) {...}
See More http://api.jquery.com/animated-selector/
Here is a function that waits for the page Html to become stable. i.e. when all animations are finished. In the example below it waits for the Html to be unchanging for 200 milliseconds and a maximum timeout of 2 seconds.
Call the function with ...
waitUntilHtmlStable(yourCallback, 200, 2000);
The function ...
waitUntilHtmlStable = function (callback, unchangedDuration, timeout, unchangedElapsed, html) {
var sleep = 50;
window.setTimeout(function () {
var newHtml = document.documentElement.innerHTML;
if (html != newHtml) unchangedElapsed = 0;
if (unchangedElapsed < unchangedDuration && timeout > 0)
waitUntilHtmlStable(callback, unchangedDuration, timeout - interval, unchangedElapsed + interval, newHtml);
else
callback();
}, sleep);
};
In my case I wanted to be sure new elements where present. If you want to track animation movement then change the document.documentElement.innerHTML to
JSON.stringify(Array.prototype.slice.call(document.documentElement.getElementsByTagName("*"), 0)
.map(function(e) {
var x = e;
var r = x.getBoundingClientRect();
while (r.width == 0 || r.height == 0) {
x = x.parentNode;
r = x.getBoundingClientRect();
}
return r;
}));
There is an unprefixed transitionstart event in IE10+. It is even cancelable.
https://msdn.microsoft.com/library/dn632683%28v=vs.85%29.aspx
On animation.css i found this.
You can also detect when an animation ends:
$('#yourElement').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', doSomething);
read full doc here
you could use Jquery which would be much easier for example you could use .animate like this
(function(){
var box = $('div.box')
$('button').on('click', function(){
box.animate({ 'font-size' : '40px'})
.animate({'color': 'red'});
})
})();
or simply do a callback function

Adding listener for position on screen

I'd like to set something up on my site where when you scroll within 15% of the bottom of the page an element flyouts from the side... I'm not sure how to get started here... should I add a listener for a scroll function or something?
I'm trying to recreate the effect at the bottom of this page: http://www.nytimes.com/2011/01/25/world/europe/25moscow.html?_r=1
update
I have this code....
console.log(document.body.scrollTop); //shows 0
console.log(document.body.scrollHeight * 0.85); //shows 1038.7
if (document.body.scrollTop > document.body.scrollHeight * 0.85) {
console.log();
$('#flyout').animate({
right: '0'
},
5000,
function() {
});
}
the console.log() values aren't changing when I scroll to the bottom of the page. The page is twice as long as my viewport.
[Working Demo]
$(document).ready(function () {
var ROOT = (function () {
var html = document.documentElement;
var htmlScrollTop = html.scrollTop++;
var root = html.scrollTop == htmlScrollTop + 1 ? html : document.body;
html.scrollTop = htmlScrollTop;
return root;
})();
// may be recalculated on resize
var limit = (document.body.scrollHeight - $(window).height()) * 0.85;
var visible = false;
var last = +new Date;
$(window).scroll(function () {
if (+new Date - last > 30) { // more than 30 ms elapsed
if (visible && ROOT.scrollTop < limit) {
setTimeout(function () { hide(); visible = false; }, 1);
} else if (!visible && ROOT.scrollTop > limit) {
setTimeout(function () { show(); visible = true; }, 1);
}
last = +new Date;
}
});
});
I know this is an old topic, but the above code that received the check mark was also triggering the $(window).scroll() event listener too many times.
I guess twitter had this same issue at one point. John Resig blogged about it here: http://ejohn.org/blog/learning-from-twitter/
$(document).ready(function(){
var ROOT = (function () {
var html = document.documentElement;
var htmlScrollTop = html.scrollTop++;
var root = html.scrollTop == htmlScrollTop + 1 ? html : document.body;
html.scrollTop = htmlScrollTop;
return root;
})();
// may be recalculated on resize
var limit = (document.body.scrollHeight - $(window).height()) * 0.85;
var visible = false;
var last = +new Date;
var didScroll = false;
$(window).scroll(function(){
didScroll = true;
})
setInterval(function(){
if(didScroll){
didScroll = false;
if (visible && ROOT.scrollTop < limit) {
hideCredit();
visible = false;
} else if (!visible && ROOT.scrollTop > limit) {
showCredit();
visible = true;
}
}
}, 30);
function hideCredit(){
console.log('The hideCredit function has been called.');
}
function showCredit(){
console.log('The showCredit function has been called.');
}
});
So the difference between the two blocks of code is when and how the timer is called. In this code the timer is called off the bat. So every 30 millaseconds, it checks to see if the page has been scrolled. if it's been scrolled, then it checks to see if we've passed the point on the page where we want to show the hidden content. Then, if that checks true, the actual function then gets called to show the content. (In my case I've just got a console.log print out in there right now.
This seems to be better to me than the other solution because the final function only gets called once per iteration. With the other solution, the final function was being called between 4 and 5 times. That's got to be saving resources. But maybe I'm missing something.
bad idea to capture the scroll event, best to use a timer and every few milliseconds check the scroll position and if in the range you need then execute the necessary code for what you need
Update: in the past few years the best practice is to subscribe to the event and use a throttle avoiding excessive processing https://lodash.com/docs#throttle
Something like this should work:
$(window).scroll(function() {
if (document.body.scrollTop > document.body.scrollHeight * 0.85) {
// flyout
}
});
document.body.scrollTop may not work equally well on all browsers (it actually depends on browser and doctype); so we need to abstract that in a function.
Also, we need to flyout only one time. So we can unbind the event handler after having flyed out.
And we don't want the flyout effect to slow down scrolling, so we will run our flytout function out of the event loop (by using setTimeout()).
Here is the final code:
// we bind the scroll event, with the 'flyout' namespace
// so we can unbind easily
$(window).bind('scroll.flyout', (function() {
// this function is defined only once
// it is private to our event handler
function getScrollTop() {
// if one of these values evaluates to false, this picks the other
return (document.documentElement.scrollTop||document.body.scrollTop);
}
// this is the actual event handler
// it has the getScrollTop() in its scope
return function() {
if (getScrollTop() > (document.body.scrollHeight-$(window).height()) * 0.85) {
// flyout
// out of the event loop
setTimeout(function() {
alert('flyout!');
}, 1);
// unbind the event handler
// so that it's not call anymore
$(this).unbind('scroll.flyout');
}
};
})());
So in the end, only getScrollTop() > document.body.scrollHeight * 0.85 is executed at each scroll event, which is acceptable.
The flyout effect is ran only one time, and after the event has returned, so it won't affect scrolling.

Categories

Resources