Action based on visiblity and duration of element in viewport - javascript

I am using IntersectionObserver to know whether a particular element is visible to the user or not. If it is visible then I am fetching some data from the server-side. Now I would like to know whether there is any API/approach to know how long an element is visible to the user.
The use case that I am trying to solve is this - If an element is visible in the viewport for 5secs then fetch some data from the server.
Thanks in advance.

const scrollArea = document.querySelector('.scroll-area');
let executeApiCall = null;
const observer = new IntersectionObserver( event => {
console.log(`intersect`, event);
if ( event[0].intersectionRatio === 0 ) {
console.log('element not in viewport');
clearTimeout(executeApiCall);
} else {
console.log('element is in viewport');
if(!executeApiCall) {
executeApiCall = setTimeout(() => {
console.log('api call');
}, 5000);
}
}
}, {
root: scrollArea
});
See: https://stackblitz.com/edit/intersection-observer-hynpuj

If return true
after setTimeout execute your next code...
let elem = document.getElementById("app");
function isInViewportFunc(el){
var rect = el.getBoundingClientRect();
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top < window.innerHeight;
}
console.log(
isInViewportFunc(elem)
)
<br/>
<br/><br/>
<br/>
<br/><br/><br/><br/><br/><br/><br/>
<div id="app">Text</div>

Related

Maintain scroll position on expanding page with JavaScript

I am trying to maintain scroll position despite content being added to the top of the page, when you scroll to the top in a React app I am working on.
I have tried:
Getting the position of the top element on the page before the content is added and then using something like window.scrollTo(0, elementPosition)
Calculating the difference between the page height before and after the added content and scrolling back to the original position based on the difference.
Determining where to scroll based on how far the Viewport was from the bottom before the added content was displayed as that position should not be changing.
None of it worked and I am at a loss. This is the code that is responsible for creating the content, rendering a portion of the content and then adding content once you scroll to the top:
//Creating the content:
useEffect(() => {
fetch('/api/endpoint', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(res => {
setLogUsers(res.users);
setTotalAmount(res.log.length);
setMessages(limitedRender(res.log));
})
.catch(err => console.log(err))
}, [renderAmount])
return(
messages.map(e => {
return(
<Msg message = {e}/>
);
})
);
//Selecting a portion of the content and detecting when you scroll to the top:
const [totalAmount, setTotalAmount] = useState();
const [renderAmount, setRenderAmount] = useState(10);
const limitedRender = (arr) => {
if (arr.length >= renderAmount) {
return arr.slice(arr.length - (renderAmount), arr.length)
}
else {
return arr
}
}
const posTop = () => {
return (
typeof window.scrollY != 'undefined' ? window.scrollY :
document.documentElement && document.documentElement.scrollTop ?
document.documentElement.scrollTop :
document.body.scrollTop ? document.body.scrollTop : 0
);
}
document.addEventListener('scroll', () => {
if (posTop() === 0) {
if (totalAmount - renderAmount >= 10) {
setRenderAmount(renderAmount + 10);
}
else if (totalAmount - renderAmount < 10) {
setRenderAmount(renderAmount + (totalAmount - renderAmount));
}
}
})
//Also may be important. I use this bit of code to ensure that the component stays scrolled to the bottom on initial render.
const btmUpView = () => {
window.scrollTo(0, document.body.scrollHeight);
}
btmUpView();
return(
<div>
<div>
<MsgFeed/>
</div>
<div>
<input type="text"></input>
<button onClick={() => sendMsg()}>Send</button>
</div>
</div>
);

Animate on scroll - animation buildups issue

I have a text static text element that changes when user scrolls more than 600px and again when it scrolls more than 1400px
$(window).scroll(function () {
if ($(this).scrollTop() > 600) {
$('.p-circle').html('Text 1');
$('.p-circle-s').html('Text 2');
}
});
$(window).scroll(function () {
if ($(this).scrollTop() > 1400) {
$('.p-circle').html('Text 1 updated');
$('.p-circle-s').html('Text 2 updated');
}
});
How can I make a basic animation of fading for them, I tried next variants and they don't work well (it is fading 2 times)
if (scrollTop > 600 && scrollTop <= 1400) {
$('.p-circle').fadeOut(500, function() {
$(this).text('Text 1').fadeIn(500);
});
$('.p-circle-s').fadeOut(500, function() {
$(this).text('Text 2').fadeIn(500);
});
} else if (scrollTop > 1400 && scrollTop <= 2100) {
$('.p-circle').fadeOut(500, function() {
$(this).text('Text 1 updated').fadeIn(500);
});
$('.p-circle-s').fadeOut(500, function() {
$(this).text('Text 2 Updated').fadeIn(500);
});
}
I think you're on the right track, however your logic in that last bit there isn't testing whether the transition already happened. Something along the lines of:
if ($(this).scrollTop() > 600 && $(this).scrollTop() <= 1400 && $(".p-circle").text() != 'Text 1')
I think I got your values wrong, but here's a fiddle to get you on the right track...
https://jsfiddle.net/64mj3k7n/3/
A better solution would be to create a function (or Class) Waypoints, to be used like:
Waypoints(window, [0, 100, 600, 1400], (wp) => {
console.log(wp); // {index:Int, value:Int}
});
which accepts three arguments:
an Element (or String selector) to scroll-watch
an Array of scrollY waypoints values
a callback Function
The callbacks is triggered only once a scroll index changes — in order to prevent buildups on every scroll Event call.
/**
* Waypoints
* Trigger callbacks on a scrollY milestone
* #url https://stackoverflow.com/a/70520524/383904
* #param {string|object} el String selector or Element object
* #param {array} waypoints Array of px from lower to higher
* #param {function} cb Callback function
*/
function Waypoints(el, waypoints = [0], cb) {
el = (typeof el === "string") ? document.querySelector(el) : el;
const wp = [...waypoints].reverse();
const wpTot = wp.length;
const getIndex = (n) => {
const i = wp.findIndex(m => m <= n);
return wpTot - 1 - (i < 0 ? wpTot : i);
};
let index = -1;
const getTask = () => {
const i = getIndex(el.scrollY);
if (index === i) return; // No task to trigger
index = i; // Update index
const value = wp[wpTot - 1 - i]; // Get the waypoint name
cb({ index, value }); // Trigger the callback
};
el.addEventListener("scroll", getTask);
window.addEventListener("DOMContentLoaded", () => getTask);
window.addEventListener("load", () => getTask);
getTask();
}
/**
* YOUR APP:
*/
const $circle1 = $('.p-circle');
const $circle2 = $('.p-circle-s');
const circle1txt = ["Text 1 scroll", "Text 1", "Text 1 updated"];
const circle2txt = ["Text 2 scroll", "Text 2", "Text 2 updated"];
Waypoints(window, [0, 600, 1400], (wp) => {
$circle1.fadeOut(500, () => {
$circle1.text(circle1txt[wp.index]).fadeIn(500);
});
$circle2.fadeOut(500, () => {
$circle2.text(circle2txt[wp.index]).fadeIn(500);
});
});
body { height: 3000px; } /* Just to force some scrollbars for this demo */
[class^="p-"] { position: fixed; }
.p-circle-s { top: 2rem; }
<div class="p-circle"></div>
<div class="p-circle-s"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

Having conditional rendering on an id of div tag

Currently I'm trying to move my image from center to left and left to right as the user scrolls down. For achieving this, I'm using useEffect to manipulate my DOM events. I want the layout in such a way that after the user has scrolled 600 pixels in height the image starts moving to the right. For this I tried conditionally rendering a div tag but I get an error in my useEffect since it doesnt recognize the other element. So how can I move my image when it reaches a certain height?
CodeSandbox: https://codesandbox.io/s/misty-sun-e6odq?file=/src/App.js
Code:
const [display, setDisplay] = useState(false);
useEffect(function onFirstMount() {
const changeBackground = () => {
let value = window.scrollY;
console.log(value);
let img = document.getElementById("moveLeft");
let img2 = document.getElementById("moveRight");
img.style.transform = `translateX(-${value * 0.5}px)`;
img2.style.transform = `translateX(${value * 0.5}px)`;
if (value > 600) {
setDisplay(true);
} else {
setDisplay(false);
}
};
window.addEventListener("scroll", changeBackground);
return () => window.removeEventListener("scroll", changeBackground);
}, []);
return (
<>
<div className="App">
<div class="inflow">
<div class="positioner">
<div class="fixed">
<div id={display?"moveRight":"moveLeft"}>
<img
alt="passport"
src="https://cdn.britannica.com/87/122087-050-1C269E8D/Cover-passport.jpg"
/>
</div>
</div>
</div>
</div>
</div>
<div className="App2">
</div>
<div className="App2"></div>
</>
As you have two calls to getElementById and you dynamically change the id based on state, you will always have an undefined element.
You could fix it like so
let img =
document.getElementById("moveLeft") ||
document.getElementById("moveRight");
const val = img.id === "moveLeft" ? -(value * 0.5) : value * 0.5;
img.style.transform = `translateX(${val}px)`;
And remove the img2 call, as there is always one of the above in the document. Example of this here: https://codesandbox.io/s/stoic-babycat-6bl5i?file=/src/App.js
You could also try to achieve it with only one image:
useEffect(
function onFirstMount() {
const changeBackground = () => {
if (!imgRef || !imgRef.current) return;
const value = window.scrollY;
if (value > 600) {
imgRef.current.style.display = `block`;
} else {
imgRef.current.style.display = `none`;
return;
}
const progress = (window.innerWidth / 100) * moveRatio;
if (
animData.current.isGoingRight &&
animData.current.currentPos >= window.innerWidth - 200
) {
animData.current.isGoingRight = false;
} else if (
!animData.current.isGoingRight &&
animData.current.currentPos <= startPosition
) {
animData.current.isGoingRight = true;
}
if (animData.current.isGoingRight)
animData.current.currentPos += progress;
else animData.current.currentPos -= progress;
imgRef.current.style.transform = `translateX(${animData.current.currentPos}px)`;
};
window.addEventListener("scroll", changeBackground);
return () => window.removeEventListener("scroll", changeBackground);
},
[imgRef]
);
This one moves in relation to the window inner width, you can see it here https://codesandbox.io/s/hidden-microservice-xb2ku?file=/src/App.js, that's just an example, I'm sure there are another approaches

How to remove jank when setting an element to a fixed position using JavaScript

I have a webpage that when scrolled down, the text freezes when it reaches the last paragraph of text but the images keep on scrolling. I've got the implementation working but there is a lot of jank when scrolling with a mouse wheel, not so much if I click and drag the scroll bar.
Are there any optimizations I can make to this code to make work as intended or is there a different way to accomplish the same task?
window.addEventListener('scroll', function (e) {
window.requestAnimationFrame(keepTextStationary);
//keepTextStationary(); // Less janky, but still horrible
});
function keepTextStationary() {
var textRect = writtenContent.getBoundingClientRect();
var imageRec = images.getBoundingClientRect();
if (textRect.bottom < window.innerHeight && document.documentElement.scrollTop > 0) {
writtenContent.style.position = 'relative';
writtenContent.style.bottom = (225 - document.documentElement.scrollTop) + 'px';
if (imagesTop === undefined) {
imagesTop = imageRec.y;
}
} else {
writtenContent.style.bottom = (225 - document.documentElement.scrollTop) + 'px';
}
if (imageRec.y >= imagesTop) {
writtenContent.style.position = '';
}
}
Here is the site so you can see the problem.
https://bowerbankninow.azurewebsites.net/exhibitions/oscar-perry-the-pheasant
You are causing layout trashing every time you call getBoundingClientRect. Try debouncing your scroll events:
var lastScrollY = 0;
var ticking = false;
function keepTextStationary() {
var textRect = writtenContent.getBoundingClientRect();
var imageRec = images.getBoundingClientRect();
if (textRect.bottom < window.innerHeight && lastScrollY > 0) {
writtenContent.style.position = 'relative';
writtenContent.style.bottom = (225 - lastScrollY) + 'px';
if (imagesTop === undefined) {
imagesTop = imageRec.y;
}
} else {
writtenContent.style.bottom = (225 - lastScrollY) + 'px';
}
if (imageRec.y >= imagesTop) {
writtenContent.style.position = '';
}
ticking = false;
}
function onScroll() {
lastScrollY = document.documentElement.scrollTop;
requestTick();
}
function requestTick() {
if (!ticking) {
requestAnimationFrame(keepTextStationary);
ticking = true;
}
}
window.addEventListener('scroll', onScroll );
See this article for in-depth explanation: https://www.html5rocks.com/en/tutorials/speed/animations/
You dont.
Relocations / styling in javascript take place after the CSS has been loaded. Bad practise. What you can do, is make it animated to make it look less horrible.
Why is pure CSS not an option ?

Scrolling child div scrolls the window, how do I stop that?

I have a div, with a scroll bar, When it reaches the end, my page starts scrolling. Is there anyway I can stop this behavior ?
You can inactivate the scrolling of the whole page by doing something like this:
<div onmouseover="document.body.style.overflow='hidden';" onmouseout="document.body.style.overflow='auto';"></div>
Found the solution.
http://jsbin.com/itajok
This is what I needed.
And this is the code.
http://jsbin.com/itajok/edit#javascript,html
Uses a jQuery Plug-in.
Update due to deprecation notice
From jquery-mousewheel:
The old behavior of adding three arguments (delta, deltaX, and deltaY)
to the event handler is now deprecated and will be removed in later
releases.
Then, event.deltaY must now be used:
var toolbox = $('#toolbox'),
height = toolbox.height(),
scrollHeight = toolbox.get(0).scrollHeight;
toolbox.off("mousewheel").on("mousewheel", function (event) {
var blockScrolling = this.scrollTop === scrollHeight - height && event.deltaY < 0 || this.scrollTop === 0 && event.deltaY > 0;
return !blockScrolling;
});
Demo
The selected solution is a work of art. Thought it was worthy of a plugin....
$.fn.scrollGuard = function() {
return this
.on( 'wheel', function ( e ) {
var event = e.originalEvent;
var d = event.wheelDelta || -event.detail;
this.scrollTop += ( d < 0 ? 1 : -1 ) * 30;
e.preventDefault();
});
};
This has been an ongoing inconvenience for me and this solution is so clean compared to other hacks I've seen. Curious to know how more about how it works and how widely supported it would be, but cheers to Jeevan and whoever originally came up with this. BTW - stackoverflow answer editor needs this!
UPDATE
I believe this is better in that it doesn't try to manipulate the DOM at all, only prevents bubbling conditionally...
$.fn.scrollGuard2 = function() {
return this
.on( 'wheel', function ( e ) {
var $this = $(this);
if (e.originalEvent.deltaY < 0) {
/* scrolling up */
return ($this.scrollTop() > 0);
} else {
/* scrolling down */
return ($this.scrollTop() + $this.innerHeight() < $this[0].scrollHeight);
}
})
;
};
Works great in chrome and much simpler than other solutions... let me know how it fares elsewhere...
FIDDLE
You could use a mouseover event on the div to disable the body scrollbar and then a mouseout event to activate it again?
E.g. The HTML
<div onmouseover="disableBodyScroll();" onmouseout="enableBodyScroll();">
content
</div>
And then the javascript like so:
var body = document.getElementsByTagName('body')[0];
function disableBodyScroll() {
body.style.overflowY = 'hidden';
}
function enableBodyScroll() {
body.style.overflowY = 'auto';
}
As answered here, most modern browsers now support the overscroll-behavior: none; CSS property, that prevents scroll chaining. And that's it, just one line!
Here's a cross-browser way to do this on the Y axis, it works on desktop and mobile. Tested on OSX and iOS.
var scrollArea = this.querySelector(".scroll-area");
scrollArea.addEventListener("wheel", function() {
var scrollTop = this.scrollTop;
var maxScroll = this.scrollHeight - this.offsetHeight;
var deltaY = event.deltaY;
if ( (scrollTop >= maxScroll && deltaY > 0) || (scrollTop === 0 && deltaY < 0) ) {
event.preventDefault();
}
}, {passive:false});
scrollArea.addEventListener("touchstart", function(event) {
this.previousClientY = event.touches[0].clientY;
}, {passive:false});
scrollArea.addEventListener("touchmove", function(event) {
var scrollTop = this.scrollTop;
var maxScroll = this.scrollHeight - this.offsetHeight;
var currentClientY = event.touches[0].clientY;
var deltaY = this.previousClientY - currentClientY;
if ( (scrollTop >= maxScroll && deltaY > 0) || (scrollTop === 0 && deltaY < 0) ) {
event.preventDefault();
}
this.previousClientY = currentClientY;
}, {passive:false});
I wrote resolving for this issue
var div;
div = document.getElementsByClassName('selector')[0];
div.addEventListener('mousewheel', function(e) {
if (div.clientHeight + div.scrollTop + e.deltaY >= div.scrollHeight) {
e.preventDefault();
div.scrollTop = div.scrollHeight;
} else if (div.scrollTop + e.deltaY <= 0) {
e.preventDefault();
div.scrollTop = 0;
}
}, false);
If I understand your question correctly, then you want to prevent scrolling of the main content when the mouse is over a div (let's say a sidebar). For that, the sidebar may not be a child of the scrolling container of the main content (which was the browser window), to prevent the scroll event from bubbling up to its parent.
This possibly requires some markup changes in the following manner:
<div id="wrapper">
<div id="content">
</div>
</div>
<div id="sidebar">
</div>
See it's working in this sample fiddle and compare that with this sample fiddle which has a slightly different mouse leave behavior of the sidebar.
See also scroll only one particular div with browser's main scrollbar.
this disables the scrolling on the window if you enter the selector element.
works like charms.
elements = $(".selector");
elements.on('mouseenter', function() {
window.currentScrollTop = $(window).scrollTop();
window.currentScrollLeft = $(window).scrollTop();
$(window).on("scroll.prevent", function() {
$(window).scrollTop(window.currentScrollTop);
$(window).scrollLeft(window.currentScrollLeft);
});
});
elements.on('mouseleave', function() {
$(window).off("scroll.prevent");
});
You can inactivate the scrolling of the whole page by doing something like this but display the scrollbar!
<div onmouseover="document.body.style.overflow='hidden'; document.body.style.position='fixed';" onmouseout="document.body.style.overflow='auto'; document.body.style.position='relative';"></div>
$this.find('.scrollingDiv').on('mousewheel DOMMouseScroll', function (e) {
var delta = -e.originalEvent.wheelDelta || e.originalEvent.detail;
var scrollTop = this.scrollTop;
if((delta < 0 && scrollTop === 0) || (delta > 0 && this.scrollHeight - this.clientHeight - scrollTop === 0)) {
e.preventDefault();
}
});
Based on ceed's answer, here is a version that allows nesting scroll guarded elements. Only the element the mouse is over will scroll, and it scrolls quite smoothly. This version is also re-entrant. It can be used multiple times on the same element and will correctly remove and reinstall the handlers.
jQuery.fn.scrollGuard = function() {
this
.addClass('scroll-guarding')
.off('.scrollGuard').on('mouseenter.scrollGuard', function() {
var $g = $(this).parent().closest('.scroll-guarding');
$g = $g.length ? $g : $(window);
$g[0].myCst = $g.scrollTop();
$g[0].myCsl = $g.scrollLeft();
$g.off("scroll.prevent").on("scroll.prevent", function() {
$g.scrollTop($g[0].myCst);
$g.scrollLeft($g[0].myCsl);
});
})
.on('mouseleave.scrollGuard', function() {
var $g = $(this).parent().closest('.scroll-guarding');
$g = $g.length ? $g : $(window);
$g.off("scroll.prevent");
});
};
One easy way to use is to add a class, such as scroll-guard, to all the elements in the page that you allow scrolling on. Then use $('.scroll-guard').scrollGuard() to guard them.
If you apply an overflow: hidden style it should go away
edit: actually I read your question wrong, that will only hide the scroll bar but I don't think that's what you are looking for.
I couldn't get any of the answers to work in Chrome and Firefox, so I came up with this amalgamation:
$someElement.on('mousewheel DOMMouseScroll', scrollProtection);
function scrollProtection(event) {
var $this = $(this);
event = event.originalEvent;
var direction = (event.wheelDelta * -1) || (event.detail);
if (direction < 0) {
if ($this.scrollTop() <= 0) {
return false;
}
} else {
if ($this.scrollTop() + $this.innerHeight() >= $this[0].scrollHeight) {
return false;
}
}
}

Categories

Resources