I was trying to move user to a certain position (which a HTMLElement with a given id is) when user click a button. So far I have tried using hashtag directly and using hash tag and add a listener listen to the router change in useEffect, and send user to the element if hashtag is detected and that element is not in the view port. They are not working at most of the time. I added a while loop to keep triggering the function, which results bringing user to the element after 20+ calls in 7-8 seconds. That is just annoying.
Does anyone successfully make the hashtag works? How can that be done?
What I have tried so far:
hashtag link: failed
hashtag link with a slash before the hashtag: failed, can't see a difference
function moving user to the location: failed, but the function itself is successfully triggered
What I am observing at the moment:
User is not moved at all
User is moved, but the process suddenly stops before user actually reach that element. I used smooth behavior as my global css, so this effect is really strange to me.
Keep checking if user is not reaching that position, and send them to it if checked. Not working as expected. It does check and send user and send again if another check still fails, but the process is too long (7-8 seconds, or longer) and if user scroll in the process they will still be dragged to the element if they haven't reach it.
Related code:
function isInViewport(el: HTMLElement) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
useEffect(() => {
const path = router.asPath;
if (path && path.includes('/#')) {
const sectionName = path.replace('/#', '');
const ele = document.getElementById(sectionName);
if (ele) {
const interval = setInterval(() => {
if (!isInViewport(ele)) {
console.log('triggered');
setTimeout(() => {
ele.scrollIntoView({ behavior: "smooth" })
}, 2000)
}
else {
clearInterval(interval);
}
}, 2000);
}
}
}, [router.asPath]);
Related
When the button is clicked, my list is replaced with a one-item list. When I click on the "return to list" button, I want to return to the same list and to the same place in the list to which I scrolled.
useEffect(() => {
if (props.specialOfferRestaurant) {
localStorage.setItem('scrollPosition', String(window.scrollY))
localStorage.setItem('restaurants', JSON.stringify(allRestaurants));
window.scrollTo(0, 0)
setAllRestaurants([props.specialOfferRestaurant])
} else if (!props.specialOfferRestaurant && localStorage.length > 0) {
setAllRestaurants(JSON.parse(localStorage.getItem('restaurants')))
window.scrollTo(0, Number(localStorage.getItem('scrollPosition')))
localStorage.clear();
}
}, [props.specialOfferRestaurant])
In this code, everything works except for the line that returns the scroll to the right place. The same line that returns the scroll to 0 also works.
The scroll position is stored in localStorage without problems. If I add this to the bottom of the code:
console.log(Number(localStorage.getItem('scrollPosition')))
I see that the correct scroll Y value is stored in localStorage
I can't figure out what's wrong.
To fix this issue, you can use the window.pageYOffset property instead of the window.scrollY property to get the scroll position of the page. The window.pageYOffset property is updated whenever the page is scrolled, either manually or programmatically.
updated answer as new description
To fix this issue, you can use a separate useEffect hook to update the list, and ensure that it is executed after the scroll position has been set. This will ensure that the list is updated before the page is scrolled, and the scroll position is correct.
useEffect(() => {
if (props.specialOfferRestaurant) {
localStorage.setItem("scrollPosition", String(window.scrollY));
localStorage.setItem("restaurants",
JSON.stringify(allRestaurants));
window.scrollTo(0, 0);
setAllRestaurants([props.specialOfferRestaurant]);
} else if (!props.specialOfferRestaurant && localStorage.length > 0) {
setAllRestaurants(JSON.parse(localStorage.getItem("restaurants")));
window.scrollTo(0, Number(localStorage.getItem("scrollPosition")));
localStorage.clear();
}
}, [props.specialOfferRestaurant]);
useEffect(() => {
if (props.specialOfferRestaurant && localStorage.length > 0) {
setAllRestaurants(JSON.parse(localStorage.getItem("restaurants")));
}
}, [props.specialOfferRestaurant]);
The correct answer was found thanks to #Md. Mohaiminul Hasan and it comes from the assumption that the list from localStorage does not have time to render and therefore the scroll cannot be applied to the middle of the list. The final, working code looks like this:
useEffect(() => {
if (!props.specialOfferRestaurant && localStorage.length > 0) {
window.scrollTo(0, Number(localStorage.getItem('scrollPosition')))
localStorage.clear();
}
}, [allRestaurants]);
useEffect(() => {
if (props.specialOfferRestaurant) {
localStorage.setItem('scrollPosition', String(window.scrollY))
localStorage.setItem('restaurants', JSON.stringify(allRestaurants));
window.scrollTo(0, 0)
setAllRestaurants([props.specialOfferRestaurant])
} else if (!props.specialOfferRestaurant && localStorage.length > 0) {
setAllRestaurants(JSON.parse(localStorage.getItem('restaurants')))
}
}, [props.specialOfferRestaurant])
Lets say I have 2 div tag in my web, the default window's view is the 1st div because it appears at the top.
Refer to the image, the full square is the height of the document that can be scrolled up down. The red box is the portion that can be seen from the device (eg: 1024 x 608). The gray part are the portion that is no longer within the view.
In the example, the tag Div 1 is not within the view anymore because the document already get scrolled to the middle. So, upon the scroll, how do I detect if the Div 1 is no longer visible?
This is what I've done so far :
const [scrollTop, setScrollTop] = useState(0);
useEffect(() => {
var div_offset = document.getElementById('div1').offsetTop
if (div_offset.top < window.innerHeight && div_offset.left < window.innerWidth) {
console.log('still visible')
} else {
console.log('not anymore')
}
// below is to detect the window scroll
function onScroll() {
let currentPosition = window.pageYOffset;
setScrollTop(currentPosition <= 0 ? 0 : currentPosition);
}
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollTop]);
When I do the scrolling, the console is always showing not anymore
I'm using the following event listener to detect mouse wheel and scroll direction:
window.addEventListener('wheel', ({ deltaY }) => {
console.log(deltaY);
if (deltaY > 0) scrollDown();
else if (deltaY < 0) scrollUp();
});
The following happens here:
2 finger touch pad scroll on Macbook triggers the event handler
deltaY keeps logging due to the scroll accelerometer
scrollDown() or scrollUp() keep firing until accelerometer stops
I only want to fire scrollUp and scrollDown once per user interaction. I therefore need to detect a new mouse scroll event, not every mouse scroll events. Is this possible?
I did try a timeout to detect if deltaY was still changing due to the accelerometer, but this wasn't sufficient because if it was still changing, a second user interaction did not trigger scrollUp or scrollDown.
Here's a CodePen of what I'm trying to achieve: https://codepen.io/anon/pen/dQmPNN
It's very close to the required functionality, but if you hammer the mouse wheel hard on the first slide, then try to scroll to the next one immediately, the timeout solution locks it so you have to wait another second or so until the timeout completes and you can continue scrolling.
This is old but I found it when looking for an answer to pretty much the same problem.
I solved the problem for my purposes, so here's my solution in case it helps anyone else.
The problem is really to define what counts as one continuous action. Without something more concrete to work with, it's just a question of timing. It's the time between events that's the key - so the algorithm is to keep accumulating events until there's a certain gap between them. All that then remains is to figure out how big the allowed gap should be, which is solution specific. That's then the maximum delay after the user stops scrolling until they get feedback. My optimum is a quarter of a second, I'm using that as a default in the below.
Below is my JavaScript, I'm attaching the event to a div with the id 'wheelTestDiv' using jQuery but it works the same with the window object, as in the question.
It's worth noting that the below looks for any onWheel event but only tracks the Y axis. If you need more axes, or specifically only want to count events towards the timer when there's a change in deltaY, you'll need to change the code appropriately.
Also worth noting, if you don't need the flexibility of tracking events against different DOM objects, you could refactor the class to have static methods and properties, so there would be no need to create a global object variable. If you do need to track against different DOM objects (I do), then you may need multiple instances of the class.
"use strict";
class MouseWheelAggregater {
// Pass in the callback function and optionally, the maximum allowed pause
constructor(func, maxPause) {
this.maxAllowedPause = (maxPause) ? maxPause : 250; // millis
this.last = Date.now();
this.cummulativeDeltaY = 0;
this.timer;
this.eventFunction = func;
}
set maxPause(pauseTime) {
this.maxAllowedPause = pauseTime;
}
eventIn(e) {
var elapsed = Date.now() - this.last;
this.last = Date.now();
if ((this.cummulativeDeltaY === 0) || (elapsed < this.maxAllowedPause)) {
// Either a new action, or continuing a previous action with little
// time since the last movement
this.cummulativeDeltaY += e.originalEvent.deltaY;
if (this.timer !== undefined) clearTimeout(this.timer);
this.timer = setTimeout(this.fireAggregateEvent.bind(this),
this.maxAllowedPause);
} else {
// just in case some long-running process makes things happen out of
// order
this.fireAggregateEvent();
}
}
fireAggregateEvent() {
// Clean up and pass the delta to the callback
if (this.timer !== undefined) clearTimeout(this.timer);
var newDeltaY = this.cummulativeDeltaY;
this.cummulativeDeltaY = 0;
this.timer = undefined;
// Use a local variable during the call, so that class properties can
// be reset before the call. In case there's an error.
this.eventFunction(newDeltaY);
}
}
// Create a new MouseWheelAggregater object and pass in the callback function,
// to call each time a continuous action is complete.
// In this case, just log the net movement to the console.
var mwa = new MouseWheelAggregater((deltaY) => {
console.log(deltaY);
});
// Each time a mouse wheel event is fired, pass it into the class.
$(function () {
$("#wheelTestDiv").on('wheel', (e) => mwa.eventIn(e));
});
Web page ...
<!DOCTYPE html>
<html>
<head>
<title>Mouse over test</title>
<script src="/mouseWheelEventManager.js"></script>
</head>
<body>
<div id="wheelTestDiv" style="margin: 50px;">Wheel over here</div>
</body>
</html>
Have you tried breaking this out into functions with a flag to check if an interaction has occurred?
For example:
// Create a global variable which will keep track of userInteraction
let shouldScroll = true;
// add the event listener, and call the function when triggered
window.addEventListener('wheel', () => myFunction());
//Create a trigger function, checking if shouldScroll is true or false.
myFunction(){
shouldScroll ? (
if (deltaY > 0) scrollDown();
else if (deltaY < 0) scrollUp();
// Change back to false to prevent further scrolling.
shouldScroll = false;
) : return;
}
/* call this function when user interaction occurs
and you want to allow scrolling function again.. */
userInteraction(){
// set to true to allow scrolling
shouldScroll = true;
}
We can avoid such a situation by delay execution and removing the events in between the delay, refer below example and have added 1000ms as delay which can be modified based on your requirements.
let scrollPage = (deltaY)=>{
console.log(deltaY);
if (deltaY > 0) scrollDown();
else if (deltaY < 0) scrollUp();
};
var delayReg;
window.addEventListener('wheel', ({ deltaY }) => {
clearTimeout(delayReg);
delayReg = setTimeout(scrollPage.bind(deltaY),1000);
});
I’m need to push a data layer event whenever a content block of a certain css class is visible for 5 seconds (a sign that the user is reading the content.
Ive used something like this:
$(window).on(‘scroll resize’, function() {
$(‘.myClass’).each(function(element) {
If (isInViewport(element)) {
setTimeout(function() {
if (isInViewport(element)) {
... // Push the data layer event.
}
}, 5000);
}
});
});
function isInViewport(element) {
... // Returns true if element is visible.
};
Just wrote this from memory, so it may not be 100% correct, but the gist is I try to:
Test visibility on every myClass element on scroll/resize
If one is visible, wait 5 seconds and check the same element one more time.
Trouble is, element is undefined when setTimeout runs isInViewport. Maybe jQuery’s .each and setTimeout are a bad match?
I managed to do this using the intersection observer. My requirements were to check if the element was 50% in view for at least a second and if so then trigger an event.
let timer;
const config = {
root: null,
threshold: 0.5 // This was the element being 50% in view (my requirements)
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
timer = setTimeout(() => {
//... push to data layer
}, 1000);
} else {
clearTimeout(timer);
}
});
}, config);
observer.observe(YourElement);
I used the jquery-visible plugin to achieve a script that will output the time (in seconds) since a particular element is in view. The output uses an interval of X seconds... out of the scroll handler.
On stop scrolling, we check all the monitored elements to know if they're in the viewport.
If an element is, we check if it already was logged in the visible_begins array on a previous scroll stop. If it isn't, we push an object containing its id and the actual time in milliseconds.
Still on scroll stop, if an element isn't in the viewport, we check if it was logged in the visible_begins and if it's the case, we remove it.
Now on an interval of X seconds (your choice), we check all the monitored elements and each that is still in viewport is outputed with the time differential from now.
console.clear();
var scrolling = false;
var scrolling_timeout;
var reading_check_interval;
var reading_check_delay = 5; // seconds
var completePartial = false; // "true" to include partially in viewport
var monitored_elements = $(".target");
var visible_begins = [];
// Scroll handler
$(window).on("scroll",function(){
if(!scrolling){
console.log("User started scrolling.");
}
scrolling = true;
clearTimeout(scrolling_timeout);
scrolling_timeout = setTimeout(function(){
scrolling = false;
console.log("User stopped scrolling.");
// User stopped scrolling, check all element for visibility
monitored_elements.each(function(){
if($(this).visible(completePartial)){
console.log(this.id+" is in view.");
// Check if it's already logged in the visible_begins array
var found = false;
for(i=0;i<visible_begins.length;i++){
if(visible_begins[i].id == this.id){
found = true;
}
}
if(!found){
// Push an object with the visible element id and the actual time
visible_begins.push({id:this.id,time:new Date().getTime()});
}
}
});
},200); // scrolling delay, 200ms is good.
}); // End on scroll handler
// visibility check interval
reading_check_interval = setInterval(function(){
monitored_elements.each(function(){
if($(this).visible(completePartial)){
// The element is visible
// Check all object in the array to fing this.id
for(i=0;i<visible_begins.length;i++){
if(visible_begins[i].id == this.id){
var now = new Date().getTime();
var readTime = ((now-visible_begins[i].time)/1000).toFixed(1);
console.log(visible_begins[i].id+" is in view since "+readTime+" seconds.")
}
}
}else{
// The element is not visible
// Remove it from thevisible_begins array if it's there
for(i=0;i<visible_begins.length;i++){
if(visible_begins[i].id == this.id){
visible_begins.splice(i,1);
console.log(this.id+" was removed from the array.");
}
}
}
});
},reading_check_delay*1000); // End interval
.target{
height:400px;
border-bottom:2px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-visible/1.2.0/jquery.visible.min.js"></script>
<div id="one" class="target">1</div>
<div id="two" class="target">2</div>
<div id="three" class="target">3</div>
<div id="four" class="target">4</div>
<div id="five" class="target">5</div>
<div id="six" class="target">6</div>
<div id="seven" class="target">7</div>
<div id="eight" class="target">8</div>
<div id="nine" class="target">9</div>
<div id="ten" class="target">10</div>
Please run the snippet in full page mode, since there is a couple console logs.
CodePen
You can use this function to check if an element is in the viewport (from this answer):
function isElementInViewport (el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}
<input id="inViewport"/>
<span style="margin-left: 9999px;" id="notInViewport">s</span>
<script>
function isElementInViewport (el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}
console.log("#inViewport in viewport: "+isElementInViewport(document.getElementById("inViewport")));
console.log("#notInViewport in viewport: "+isElementInViewport(document.getElementById("notInViewport")));
</script>
You can try using Waypoints, its a library that allows you to determine when a element enters or leaves that viewport. You pass it an event handler that accepts a direction parameter. The direction tells you whether the tracked element entered or exited the screen. Once you detect the element has entered the screen then start a timer. If you don't see and event for when the element exited the viewport then you know it has been on screen for that period of time.
I am trying to detect a scroll on my page using JavaScript. So that I can change classes and attributes of some elements when user has scrolled certain amount of page. This is my JS function:
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
fix_class = "changeColor";
if( window.pageYOffset > header_height ) {
header.classList.add(fix_class);
}
if( window.pageYOffset < header_height ) {
header.classList.remove(fix_class);
}
var change = window.setInterval(detectScroll, 5000);
}
and I am calling it when the page is loaded:
<body onload="detectScroll();">
However, I have this problem - I need to set up a really small interval so that the function gets called and the class is changed immediately. BUT then the page freezes and everything except the JS function works very slowly.
Is there any better way of achieving this in JavaScript?
Thanks for any advice/suggestion.
You are going to want to change a couple things. First, we can use onscroll instead of an interval. But you are also going to want to cache as much as possible to reduce the amount of calculations on your scroll. Even further, you should use requestAnimationFrame (or simply "debounce" in general for older browsers -- see the link). This ensures your work only happens when the browser is planning on repainting. For instance, while the user scrolls the actual scroll event may fire dozens of times but the page only repaints once. You only care about that single repaint and if we can avoid doing work for the other X times it will be all the more smoother:
// Get our header and its height and store them once
// (This assumes height is not changing with the class change).
var header = document.querySelector(".headerOrig");
var header_height = getComputedStyle(header).height.split('px')[0];
var fix_class = "changeColor";
// This is a simple boolean we will use to determine if we are
// waiting to check or not (in between animation frames).
var waitingtoCheck = false;
function checkHeaderHeight() {
if (window.pageYOffset > header_height) {
header.classList.add(fix_class);
}
if (window.pageYOffset < header_height) {
header.classList.remove(fix_class);
}
// Set waitingtoCheck to false so we will request again
// on the next scroll event.
waitingtoCheck = false;
}
function onWindowScroll() {
// If we aren't currently waiting to check on the next
// animation frame, then let's request it.
if (waitingtoCheck === false) {
waitingtoCheck = true;
window.requestAnimationFrame(checkHeaderHeight);
}
}
// Add the window scroll listener
window.addEventListener("scroll", onWindowScroll);
use onscroll instead of onload so you don't need to call the function with an interval.
Your dedectScroll function will be triggered automatically when any scroll appers if you use onscroll
<body onscroll="detectScroll();">
Your function is adding an interval recursively, you should add an event listener to the scroll event this way :
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
fix_class = "changeColor";
if( window.pageYOffset > header_height ) {
header.classList.add(fix_class);
}
if( window.pageYOffset < header_height ) {
header.classList.remove(fix_class);
}
}
window.addEventListener("scroll",detectScroll);