how to get difference between window height and scroll location? - javascript

I want to make custom infinite scroll, so when I try this
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
if(window.innerHeight-scrollPosition >100){
console.log("end")
}
but it does not work.

If your wanting to know when your 100 pixels away from the end, then you can get the current element scrollHeight and subtract the parent elements height and then subtract your extra 100.
Now compare this to the parentElements scrollTop, if it's greater then your scrollbar is within this 100px section..
Example below.. If you scroll down until your within 100 pixels of the end, the background will change silver.
document.body.innerText =
new Array(400).fill('Scroll me down, ').join('');
window.addEventListener('scroll', (e) => {
const body = document.body;
const parent = body.parentElement;
const pixelsFromBottom =
body.scrollHeight -
parent.clientHeight
-100;
body.classList.toggle('inf'
,parent.scrollTop > pixelsFromBottom);
});
.inf {
background-color: silver;
}
This will work not with just Body, but also any sub controls too, below I've created a header footer, with and a scrollable region.
const scroller = document.querySelector('main');
const target = document.querySelector('.content');
target.innerText =
new Array(400).fill('Scroll me down, ').join('');
scroller.addEventListener('scroll', (e) => {
const body = target;
const parent = body.parentElement;
const pixelsFromBottom =
body.scrollHeight -
parent.clientHeight
-100;
parent.classList.toggle('inf'
,parent.scrollTop > pixelsFromBottom);
});
html, body {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
background-color: cyan;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
main {
position: relative;
flex: auto;
overflow-y: scroll;
background-color: white;
}
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.inf {
background-color: silver;
}
<header>This is a header</header>
<main><div class="content">main</div></main>
<footer>This is the footer</footer>

Related

Using getBoundingClientRect() when resizing the window

I have this navbar and everytime I click an option in the navbar the absolute positioned indicator gets the position of the option on the left and the width with the help of getBoundingClientRect() and it is moved to the target.
The problem is when I resize the window the indicator changes it's position and moves away.To stay in the same place when I resize the window I applied an eventListener to the window and everytime is resized I get the new values of left and width with getBoundingClientRect().
It works but I wonder if that is a bad way to do it because of the calculations that happen everytime the window is resized and if that is the case what is a better way to do this.
Here is the code:
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.getBoundingClientRect().left}px`;
navigator.style.width = `${firstOption.getBoundingClientRect().width}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.getBoundingClientRect().left}px`;
navigator.style.width = `${e.target.getBoundingClientRect().width}px`;
};
});
window.addEventListener('resize', function() {
let navOptionActive = nav.querySelector('.nav-option-active');
navigator.style.left = `${navOptionActive.getBoundingClientRect().left}px`;
navigator.style.width = `${navOptionActive.getBoundingClientRect().width}px`;
});
* {
margin: 0;
padding: 0;
}
nav {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin: 100px auto;
padding: 7vh 30vw;
width: auto;
background:#eeeeee;
}
.nav-option {
padding: 0 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
You can make your <nav> element tightly wrap the buttons, then position the underline relative to the <nav>. A new wrapper <div> around the <nav> takes care of the margins and gray background. Instead of getBoundingClientRect() you then need to use offsetLeft and offsetWidth.
Note that this doesn't handle the changes in response to your #media query. For that, you could add a resize listener that specifically only handles changes across the 1200px threshold. Alternatively, you could reparent the underline to be a child of the actual nav button while it's not animating. Neither solution is great, but both would get the job done.
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.offsetLeft}px`;
navigator.style.width = `${firstOption.offsetWidth}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.offsetLeft}px`;
navigator.style.width = `${e.target.offsetWidth}px`;
};
});
* {
margin: 0;
padding: 0;
}
.nav-wrapper {
margin: 100px 0;
display: flex;
justify-content: center;
background: #eeeeee;
}
nav {
position: relative;
display: flex;
}
.nav-option {
padding: 7vh 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<div class="nav-wrapper">
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
</div>
If you have to use getBoundingClientRect (which honestly has nothing wrong with it), you can throttle the call, so that only the last resize after sufficient time has passed will execute. There are zillion ways of doing this, I will leave one example:
window.onresize = (function(id = null, delay = 600, oEvent = null){
return function fire(event){
return (new Promise(function(res,rej){
if (id !== null){
oEvent = event;
rej("busy");
return;
}
id = setTimeout(function(){
res(oEvent || event);
},delay);
})).then(function(event){
id = null;
console.log(event, "do getBoundingClientRect call");
}).catch(function(){void(0);});
};
}());
Replace console.log with what you want to do.
Your other option is to switch to intersection observer, if you can restructure your rendering logic. That will require some work

Fix container at bottom till window.pageYOffset is greater than original position of container

I am fixing the position of container stickyDiv at the bottom of viewport once user scrolls past top of stickInDiv, till its original position ( that I calculate from the top of document) is first revealed at the bottom of viewport. After that the container should remain at its original position. On scrolling up, it should again get fixed to the bottom of viewport once its original position hits the bottom of viewport.
Current behaviour: stickyDiv does not stay at its original position once its original position is reached at the bottom of viewport.
window.onscroll = function() {
scrollFunction();
};
function getTop(elem) {
const box = elem.getBoundingClientRect();
const body = document.body;
const docEl = document.documentElement;
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
const clientTop = docEl.clientTop || body.clientTop || 0;
const top = box.top + scrollTop - clientTop;
return Math.round(top)
}
const stickyDiv = document.getElementById('stickyDiv');
const stickyElemTopFromDocumentTop = getTop(stickyDiv);
const Height = stickyDiv.clientHeight; /* height of the sticky element */
const stickInDiv = document.getElementById('stickInDiv');
const stickInDivOffsetTop = stickInDiv.offsetTop;
function scrollFunction() {
if (window.pageYOffset > stickyElemTopFromDocumentTop) {
stickyDiv.classList.remove('sticky');
} else if (window.pageYOffset > stickInDivOffsetTop) {
stickyDiv.classList.add('sticky');
} else {
stickyDiv.classList.remove('sticky');
}
}
#contentAtTop {
height: 100px;
background-color: blue;
opacity: 0.2;
color: white;
}
#contentBeforeStickyDiv {
height: 1200px;
background-color: green;
color: white;
opacity: 0.8;
}
#stickyDiv {
color: white;
background-color: grey;
height: 40px;
}
#contentAfterStickyDiv {
height: 400px;
background-color: purple;
color: white;
opacity: 0.8;
}
.sticky {
position: fixed;
bottom: 0;
width: 100%;
}
<body>
<div id="contentAtTop">Content At Top</div>
<div id="stickInDiv">
<div id="contentBeforeStickyDiv">Content Before Sticky Div</div>
<div id="stickyDiv">Sticky Container</div>
<div id="contentAfterStickyDiv">Content After Sticky Div</div>
</div>
</body>

Two conditions for scroll offset and element

What I trying to do is, show .box-tocart when scroll top bigger than .product-info-main offset top and also if reached to .page-footer should hide but I couldn't mix these conditions together, each condition work separately but not working together with || or &&
var target = $('.product-info-main').offset().top;
$(window).scroll(function() {
var footer = $('.page-footer').offset().top;
var element = $('.box-tocart').offset().top;
if (($(window).scrollTop() >= target) || (element >= footer)) {
$('.box-tocart').show();
} else {
$('.box-tocart').hide();
}
});
body {
height: 2000px;
}
#nothing {
height: 100px;
background: red;
}
.product-info-main {
height: 1000px;
}
.box-tocart {
height: 30px;
background: green;
display: none;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
.page-footer {
background: blue;
height: 100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="nothing"></div>
<div class="product-info-main">
<div class="box-tocart"></div>
</div>
<div class="page-footer"></div>
Goal: show .box-tocart if scroll top bigger than .product-info-main offset top, else hide. Also if reached to .page-footer hide, else show, but I want these two conditions together, but couldn't make it work.
The problem with current snippet is, it not hide .box-tocart after reach .page-footer
Simple explanation: green div should show after red div, else hide and should hide after
reach to blue div else hide.
You need to change the condition to:
var scrollTop = $(window).scrollTop();
var windowHeight = $(window).height();
if ((scrollTop >= target) && (scrollTop + windowHeight <= footer)) {
// ...
}
Updated example:
var target = $('.product-info-main').offset().top;
$(window).scroll(function() {
var footer = $('.page-footer').offset().top;
var element = $('.box-tocart').offset().top;
var scrollTop = $(window).scrollTop();
var windowHeight = $(window).height();
if ((scrollTop >= target) && (scrollTop + windowHeight <= footer)) {
$('.box-tocart').show();
} else {
$('.box-tocart').hide();
}
});
body {
height: 2000px;
}
#nothing {
height: 100px;
background: red;
}
.product-info-main {
height: 1000px;
}
.box-tocart {
height: 30px;
background: green;
display: none;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
.page-footer {
background: blue;
height: 100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="nothing"></div>
<div class="product-info-main">
<div class="box-tocart"></div>
</div>
<div class="page-footer"></div>
You should use $(window).scrollTop() instead of element so your OR condition should be like if (... || $(window).scrollTop() >= footer, that’s because the scroll position is all relative to the window view and not to the cart box
I hope it can help you.

Configure IntersectionObserver to change value of *.isIntersecting based on X pixels

I have intersection observer object it works, but I want it to notify my once some element is 100pixels over or at bottom of intersection point.
With default config it just changes value of .isIntersection once the element is exactly in view. But I want to do some stuff when elements are 100pixels above or below the viewport.
This is my code:
var iObserver = new IntersectionObserver(function(element) {
console.log('elementi', element); // I want to trigger here when elementi is 100px or less of distance to the viewport.
});
var el;
for (var i = 0; i < elements.length; i++) {
el = elements[i];
console.log('eli', el);
iObserver.observe(el);
}
UPDATE
Thanks to user for answer I used this and it worked:
var iObserver = new IntersectionObserver(function(entryEvent) {
//...
}, {'rootMargin': '100px 0px 100px 0px'});
You can define the rootMargin top and bottom in the options you pass to the observer.
In the demo, hover the red rectangle, when it reaches a distance of 10px from the .container the observer is called:
const options = {
root: document.querySelector('.container'),
rootMargin: '10px 0px 10px 0px',
};
let i = 0;
const iObserver = new IntersectionObserver((entries) => console.log(`intersection ${i++}`), options);
iObserver.observe(document.querySelector('.element'));
.container {
position: relative;
height: 20px;
background: lightblue;
}
.element {
position: absolute;
top: calc(100% + 30px);
height: 100px;
width: 100px;
background: red;
margin-bottom: 20px;
transition: top 5s;
}
.element:hover {
top: calc(100% - 30px);
}
.as-console-wrapper {
height: 50px;
}
<div class="container">
<div class="element">1</div>
</div>

Built a scroll controller, need to reverse it

My code allows scrolling vertically in the bottom section to control scrolling horizontally in the top section.
My jsfiddle
You'll see the colors shift through a gradient. Works pretty well. Problem is that I can't quite seem to get the inverse to work. Scrolling horizontally in the top controls scrolling in the bottom.
Any ideas?
Here's the script that makes it work:
// Add event listener for scrolling
$("#bottom").on("scroll", function bottomScroll() {
var scrolledleft = parseInt($("#bottom").scrollTop()) * 1;
console.log(scrolledleft + scrolledright)
$("#top").scrollLeft(scrolledleft + scrolledright)
})
//Move right column to bottom initially
$("#top").scrollLeft($("#top").height())
//Get actual distance scrolled
var scrolledright = parseInt($("#top").scrollLeft())
Your event handlers need to temporarily cancel each other so that they don't both fire at once. You want to calculate your position percentage based on the current scrollLeft / (width of child div - width of container), then apply that percentage to the other element, and likewise for top/height. Also I changed the height of #top to 50% in CSS.
var handler = function (e) {
var src = e.target;
// the first element that triggers this function becomes the active one, until it's done
if (!activeScroller) activeScroller = src.id;
else if (activeScroller != src.id) return;
var $b = $("#bottom");
var $t = $("#top");
var scrollH = $("#bottom-content").height() - $b.height();
var scrollW = $("#top-content").width() - $t.width();
var scrollPct = 0;
if (src.id == "top") {
if (scrollW > 0) {
scrollPct = $t.scrollLeft() / scrollW;
}
$b.scrollTop(scrollH * scrollPct);
} else {
if (scrollH > 0) {
scrollPct = $b.scrollTop() / scrollH;
}
$t.scrollLeft(scrollW * scrollPct);
}
// give all animations a chance to finish
setTimeout(function () { activeScroller = ""; }, 100);
};
var activeScroller = "";
$("#top,#bottom").on("scroll", handler);
#top {
position: absolute;
margin: auto;
top: 0;
right: 0;
left: 0;
width: 100%;
height: 50%;
position: fixed;
overflow: auto;
background: red;
}
#top-content {
height: 100%;
width: 2000px;
background: linear-gradient(90deg, red, blue);
}
#bottom {
position: absolute;
margin: auto;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 50%;
position: fixed;
overflow: auto;
background: green;
z-index: 100;
}
#bottom-content {
height: 2000px;
width: 100%;
background: linear-gradient(0deg, orange, green);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="top">
<div id="top-content"></div>
</div>
<div id="bottom">
<div id="bottom-content"></div>
</div>
Check out this:
https://jsfiddle.net/1p7gp72h/1/
I'm not sure what your end goal is here.
$("#top").on("scroll", function topScroll() {
var scrolledleft = parseInt($("#top").scrollTop()) * 1;
$("#bottom").scrollLeft(scrolledleft + scrolledright)
});
#top {
top: 0;
right: 0;
left: 0;
width: 5000px;
height: 100%;
overflow: auto;
background: red;
overflow-x: scroll;
overflow-y: hidden;
white-space:nowrap;
}
Scroll to left ::
$('div').scrollLeft(1000);
Scroll back to normal/ scroll to right ::
$('div.slick-viewport').scrollLeft(-1000);

Categories