Check for scrollTo to finish - javascript

I have an element that is scrollable. I also have a function that scrolls to a specific position. I would like to call a function when the scrollTo is finished.
Plunkr example
var x = document.querySelector('.container');
$scope.scrollTo = function() {
x.scrollTo({
top: 300 ,
behavior: 'smooth'
});
};
// do something when scrollTo is finished

By checking the position of the element I am scrolling to and comparing that to the current scroll position of the container you can see when the scrolling action is finished.
function isScrollToFinished() {
const checkIfScrollToIsFinished = setInterval(() => {
if (positionOfItem === scrollContainer.scrollTop) {
// do something
clearInterval(checkIfScrollToIsFinished);
}
}, 25);
}
The interval checks if the position of the scroll container is equal to the position of the element I'm scrolling to. Then do a action and clear the interval.

I suggest checking that the scroll movement is gone.
Less parameter, no risk, and works for extremes positions.
let position = null
const checkIfScrollIsStatic = setInterval(() => {
if (position === window.scrollY) {
clearInterval(checkIfScrollIsStatic)
// do something
}
position = window.scrollY
}, 50)

I modified #DoiDor's answer to ensure that the position is rounded and include a timeout fallback just in case. Otherwise the position may never be reached exactly, and the promise would never resolve.
async function scrollToPosition(container, position) {
position = Math.round(position);
if (container.scrollTop === position) {
return;
}
let resolveFn;
let scrollListener;
let timeoutId;
const promise = new Promise(resolve => {
resolveFn = resolve;
});
const finished = () => {
container.removeEventListener('scroll', scrollListener);
resolveFn();
};
scrollListener = () => {
clearTimeout(timeoutId);
// scroll is finished when either the position has been reached, or 100ms have elapsed since the last scroll event
if (container.scrollTop === position) {
finished();
} else {
timeoutId = setTimeout(finished, 100);
}
};
container.addEventListener('scroll', scrollListener);
container.scrollTo({
top: position,
behavior: 'smooth',
});
return promise;
}

I'm using a solution similar to this:
function scrollElementTo(el, position) {
return new Promise((resolve) => {
const scrollListener = (evt) => {
if (typeof evt === 'undefined') {
return;
}
const target = evt.currentTarget;
if (target.scrollTop === position) {
target.removeEventListener('scroll', scrollListener);
resolve();
}
};
el.addEventListener('scroll', scrollListener);
// scroll to desired position (NB: this implementation works for
// vertical scroll, but can easily be adjusted to horizontal
// scroll as well)
el.scrollTo(0, position);
});
}
const scrollContainer = document.querySelector('#theScrollContainer');
// desired y coords for scroll
const yDesiredScroll = 234;
scrollElementTo(scrollContainer, yDesiredScroll).then(() => {
// do something here
});

Related

Javascript variable reference and/or copy

i have e video that i am scrolling with my mousewheel.
Everything is working fine but i would like to check if the user is still scrolling and if it is not so after 10 seconds i would like the video/scrollposition to set-back to zero.
The set-back is working but it is not happing after the "Interval". It is jumping back immediately.
This is my code so far:
const action = document.querySelector(".action");
const video = action.querySelector("video");
//Scroll Magic scenes
const controller = new ScrollMagic.Controller();
let scene = new ScrollMagic.Scene({
duration: 200000, //64000
triggerElement: action,
triggerHook: 0
})
.addIndicators()
.setPin(action)
.addTo(controller);
//Scroll Magic video animation
let accelAmount = 0.1;
let scrollpos = 0;
let currentTime = video.currentTime;
let lastcurrentTime;
let delay = 0;
scene.on("update", e => {
scrollpos = e.scrollPos / 3000; //the higher the number, the slower
});
//Move
setInterval(() => {
delay += (scrollpos - delay) * accelAmount;
video.currentTime = delay;
console.log(video.currentTime + " reading");
lastcurrentTime = video.currentTime;
}, 33.3);
//check if is still scrolling after x seconds
setInterval(checkTime, 10000);
//funktion to execute the check
function checkTime() {
console.log("waiting for new reading");
console.log(video.currentTime + " newreading");
currentTime = video.currentTime;
if (currentTime === lastcurrentTime) {
//is not scrolling -- go to start
//video.currentTime = 0;
window.scrollTo(0, 0);
}
}
You are updating the lastcurrentTime every 33.3ms and you're calling checkTime every 10s. So almost every time you call the checkTime, the lastcurrentTime will be synced with the video.currentTime.
I think that I would try something like this:
let setbackTimeout
scene.on("update", e => {
clearTimeout(setbackTimeout);
scrollpos = e.scrollPos / 3000; //the higher the number, the slower
setbackTimeout = setTimeout(checkTime, 10000);
});
every time you get an update you clear the countdown to setback and create another that you execute 10s later.
Listen to the scroll event listener. With a debounce-like function you can start a timeout that will run whenever the use stops scrolling.
The example below uses the aformentioned technique and shows a message whenever the user stops scrolling for 1 second.
function scrollTracker(delay, callback) {
let timeout = null;
return function(...args) {
if (timeout !== null) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(callback, delay, ...args)
};
}
const tracker = scrollTracker(1000, () => {
console.log('User stopped scrolling. Handle your video here');
});
window.addEventListener('scroll', tracker);
#page {
height: 20000px;
}
<div id="page"></div>
I think your concept is wrong. I made a little demo how to implement it. It will print the message after 4 sec "inactivity" on button.
const btn = document.querySelector("button")
let myInterval = setInterval(myPrint, 4000)
btn.addEventListener("click", () => {
clearInterval(myInterval)
myInterval = setInterval(myPrint, 4000)
})
function myPrint(){
clearInterval(myInterval)
console.log("programming <3")
}
<button>Click me!</button>

How to trigger a JavaScript function only when the user scrolls the page

I need to add positon: fixed to .box only if the user scrolls the page.
The important point here is that the element with the class .box get generated only after the user scrolls the page.
This is what I came up with:
window.addEventListener('scroll', () => {
const myDiv = document.querySelector('.box')
if (myDiv) {
if (myDiv.style.position !== 'fixed') {
myDiv.style.position = 'fixed'
}
}
})
The problem with my code is that the scroll event is now going to fire a million events all the time and kill performance.
What would be the right way to achieve the above without firing scroll event again and again.
After the box has been inserted into the DOM, you can query the box and add the fixed position style to it.
If you're doing this often then here is a more general function that will (given a selector, e.g. .box) wait for an element to be inserted into the DOM:
function waitForElement(selector) {
let element = null;
let handled = false;
let nextFrame;
return (callback) => {
function tick() {
element = document.querySelector(selector);
if (element === null) {
nextFrame = requestAnimationFrame(tick);
}
if (element !== null && !handled) {
handled = true;
callback(element);
}
}
cancelAnimationFrame(nextFrame);
requestAnimationFrame(tick);
};
}
Usage:
const waitForBox = waitForElement(".box");
waitForBox((box) => {
box.style.position = "fixed";
});
Demo:
// Demo section, ignore this
const plane = document.querySelector(".plane");
const box = document.createElement("div");
box.className = "box";
box.style.width = "100px";
box.style.height = "100px";
box.style.background = "crimson";
function waitForElement(selector) {
let element = null;
let handled = false;
let nextFrame;
return (callback) => {
function tick() {
element = document.querySelector(selector);
if (element === null) {
nextFrame = requestAnimationFrame(tick);
}
if (element !== null && !handled) {
handled = true;
callback(element);
}
}
cancelAnimationFrame(nextFrame);
requestAnimationFrame(tick);
};
}
// A function to call inside scroll callback that will
// wait for the .box element to be found in the DOM
const waitForBox = waitForElement(".box");
let boxAppended = false;
document.addEventListener("scroll", () => {
// Appending the box to the DOM, ignore this
if (!boxAppended) {
boxAppended = true;
plane.appendChild(box);
}
// Callback will be fired once when the .box
// element is found in the DOM
waitForBox((box) => {
box.style.position = "fixed";
});
});
body {
padding: 0;
height: 100vh;
overflow: auto;
}
.plane {
height: 200vh;
}
<div class="plane"></div>
You can for example use requestAnimationFrame() or setTimeout() to prevent the scroll event from firing too many times.
Here is an example of how to do this with requestAnimationFrame() from the Mozilla scroll event documentation:
let lastKnownScrollPosition = 0;
let ticking = false;
function doSomething(scrollPos) {
// Do something with the scroll position
}
document.addEventListener('scroll', function(e) {
lastKnownScrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
});

Element.scrollIntoView(); Not working in Chrome and Edge, Works in Firefox?

Let me preface by saying I am relatively green in respect to JavaScript. However, I am trying to create a JavaScript function that will listen for a wheel event, determine the direction of the scroll and scroll the next part of the page into view. Similar to a swipe gesture. I have it working in Firefox as intended, user scrolls down the page moves down, up the page moves up. However, in Chrome and Edge, the element.scrollIntoView() function seems to be called, but nothing happens.
I am using Blazor with JSInterop to get the page to scroll.
Here is the gist of the file:
// scrolldetection.js //
window.scrolldetection {
scrollSetup: function (elementId) {
autoscroller(elementId);
}
}
function autoscroller(elementId) {
// Set up vars
var idList = document.getElementById(elementId).children.id;
var idListIndex = // Gets current index based on current location on page
var curDirection;
document.addEventListener("wheel", function () {
var currentPos = window.scrollY;
if (!curDirection) {
// Check for what direction user scrolls
idListIndex = // Function that determines new index
scrollScreen(idList, idListIndex);
}
var timerId = setTimeout(function () {
curDirection = undefined;
clearTimeout(timerId);
}, 700);
}
}
function scrollScreen(idList, curIndex) {
console.log("list index: " + curIndex);
element = document.getElementById(idList[curIndex]);
console.log(element.id);
element.scrollIntoView({ behavior: 'smooth' });
}
I have another function that calls scrollIntoView() on the same elements, via a button press. Works just fine.
Here is that function:
// anchorlink.js // This one works as intended //
window.anchorlink = {
scrollIntoView: function (elementId) {
var elem = document.getElementById(elementId);
if (elem) {
elem.scrollIntoView({ behavior: 'smooth' });
}
}
}
The console.log() calls within the scrollScreen() function show up properly in the console in each browser. Leading me to believe that element.scrollIntoView() is being called, but something is going wrong to make it not function appropriately. If there is any other information you need, I will be happy to provide, thanks.
After working on this some more, I decided the concept I was using wasn't as user friendly as I had hoped.
I decided to let the user scroll freely along the page and when they stop scrolling, then determine the closest DOM element and scroll to it. Works on Edge, Chrome and Firefox.
window.contentcenter = {
contentCenter: function (elementId) {
var centeringFunction = debounce(function () { autocenter(elementId) }, 200);
document.addEventListener("scroll", centeringFunction);
}
}
function autocenter(elementId) {
var currentElement = detectCurrentElement(elementId);
currentElement.scrollIntoView({ behavior: "smooth" });
}
function detectCurrentElement(elementId) {
var element = document.getElementById(elementId);
var currentPos = window.scrollY;
var contentIdList = getContentIdList(elementId);
var currentElement = closestContent(currentPos, element, contentIdList);
return currentElement;
}
function closestContent(pos, element, contentIdList) {
var contentId = Math.round(pos / (element.offsetHeight / contentIdList.length));
var currentElement = document.getElementById(contentIdList[contentId]);
return currentElement;
}
function getContentIdList(elementId) {
var idList = []
var childElements = document.getElementById(elementId).children;
for (var i = 0; i < childElements.length; i++) {
idList.push(childElements[i].id);
}
return idList;
}
function debounce(func, timeout) {
var timer;
return function () {
var context = this, args = arguments;
var later = function () {
timer = null;
func.apply(context, args);
};
clearTimeout(timer);
timer = setTimeout(later, timeout)
};
}

Is it recreate in variable memory for each scroll event?

I have an element with the name ".offer" class right above the footer inside the page. When I approach this element in the scroll movement, I want the sticky header to close.
I wrote a function in scroll event for this but I am curious about something.
When I create a function like the example below, are the variables in the function re-created in memory for each scroll move? Or what's the truth for performance in a simple page scroll event?
const stickyNearOfferClose = () => {
let scrollPos;
let header = document.querySelector("header");
const documentOffset = document.querySelector(".offer").offsetTop;
header.classList.toggle("sticky", window.scrollY);
scrollPos = window.scrollY;
if (scrollPos >= documentOffset) {
header.classList.add("hide");
} else {
header.classList.remove("hide");
}
};
window.addEventListener("scroll", function () {
stickyNearOfferClose();
});
In addition to the above question
When I want to use the above function for one or more scroll actions,
Should we use it define variables inside a function or within an object for both in terms of performance and usability?
I shared two different examples below. Which one do you think should be?
const Obj = {
scrollPos : null,
elHeader: document.querySelector("header"),
documentOffset: document.querySelector(".offer").offsetTop,
// otherfunction (){},
stickyNearOfferClose() {
Obj.elHeader.classList.toggle("sticky", window.scrollY);
if (Obj.scrollPos >= Obj.documentOffset) {
Obj.elHeader.classList.add("hide");
} else {
Obj.elHeader.classList.remove("hide");
}
},
// init(){}
};
window.addEventListener("scroll", function () {
Obj.scrollPos = window.scrollY;
requestAnimationFrame(Obj.stickyNearOfferClose);
});
// OR
const Obj = {
// variables
stickyNearOfferClose() {
let scrollPos;
const elHeader = document.querySelector("header");
const elOffer = document.querySelector(".offer");
const documentOffset = elOffer.offsetTop;
let stickyClose = () => {
elHeader.classList.toggle("sticky", window.scrollY);
if (scrollPos >= documentOffset) {
elHeader.classList.add("hide");
} else {
elHeader.classList.remove("hide");
}
};
window.addEventListener("scroll", () => {
scrollPos = window.scrollY;
requestAnimationFrame(stickyClose);
});
},
spyScrolling() {
let scrollPos;
const sections = document.querySelectorAll(".hero");
let scrollActiveUrl = () => {
for (let s in sections) {
if (
sections.hasOwnProperty(s) &&
sections[s].offsetTop <= scrollPos + 150
) {
const id = sections[s].id;
document.querySelector(".active").classList.remove("active");
document.querySelector(`a[href*=${id}]`).classList.add("active");
}
}
};
window.addEventListener("scroll", () => {
scrollPos = document.documentElement.scrollTop || document.body.scrollTop;
requestAnimationFrame(scrollActiveUrl);
});
}
init(){
this.stickyNearOfferClose();
this.spyScrolling()
}
};
Yes, the variables in the function will be recreated on every scroll events. But in your case, you can put some variables outside of the function to save a bit.
As per MDN, you shouldn't call expensive DOM manipulations directly in the function. Instead, it is recommended to throttle the event using requestAnimationFrame(), setTimeout(), or a CustomEvent. You can learn more at here.
Following is the improved example of your code:
let scrollPos;
const elHeader = document.querySelector("header");
const elOffer = document.querySelector(".offer");
// Note: I'm assuming that `documentOffset` don't need to be updated on every scroll events.
// Update it as `scrollPos` if it need to be updated on every scroll events.
const documentOffset = elOffter.offsetTop;;
const stickyNearOfferClose = () => {
// Note: Please move the following line in the scroll event
// if it's also needed to be called on every scroll events.
elHeader.classList.toggle("sticky", window.scrollY);
if (scrollPos >= documentOffset) {
elHeader.classList.add("hide");
} else {
elHeader.classList.remove("hide");
}
};
window.addEventListener("scroll", function () {
scrollPos = window.scrollY;
requestAnimationFrame(stickyNearOfferClose);
});

How re-calculate the window container/image width on resize?

I have a method/function in React where i swipe to the left and right on click with a translate value from CSS. However, on render the screens are loaded, and will move to the left or right and translate value will be set correctly. But the problem is, when i try to resize the browser the calculation doesn't work.
I have tried creating a onClick={this.ResizeSlider} and from there run the function but doesn't work.
My code:
NextSlide = () => {
const {
currentIndex,
data
} = this.state;
const slideWidth = this.slideWidth();
// Exiting the method early if we are at the end of the images array.
// We also want to reset currentIndex and translateValue, so we return
// to the first image in the array.
if(currentIndex === data.length - 1) {
return this.setState({
currentIndex: 0,
translateValue: 0
})
}
// This will not run if we met the if condition above
this.setState(NextState => ({
currentIndex: NextState.currentIndex + 1,
translateValue: NextState.translateValue + -(slideWidth)
}));
}
slideWidth = () => {
const { slideClass } = this.state;
return document.querySelector(slideClass).clientWidth
}
You need to add an eventListener so you know when the screen size has changed.
Try something like this:
componentDidMount() {
this.dimensionsChange();
window.addEventListener("resize", this.dimensionsChange());
}
And you can access the width/height values in the dimensionsChange() method using:
var w = window.innerWidth;
var h = window.innerHeight;
Don't forget to remove the event listener when your component unmounts:
componentWillUnmount() {
window.removeEventListener("resize", this.dimensionsChange());
}

Categories