Detects when an element cross any div of a certain class - javascript

I have a dark-colored menu that sometimes crosses over sections with the same dark background, so am trying to switch its class to change its colors everytime it crosses over dark colored sections.
$(window).scroll(function(){
var fixed = $("section.fixed-header");
var fixed_position = $("section.fixed-header").offset().top;
var fixed_height = $("section.fixed-header").height();
var toCross_position = $(".dark").offset().top;
var toCross_height = $(".dark").height();
if (fixed_position + fixed_height < toCross_position) {
fixed.removeClass('light-menu');
} else if (fixed_position > toCross_position + toCross_height) {
fixed.removeClass('light-menu');
} else {
fixed.addClass('light-menu');
}
});
This works fine when I only have one div with the dark class inside the same page. However, if there are several different divs with the dark class inside the same page, it will only work for the first div. How could I include all the other divs with the same dark class in here?

Instead of listening to scroll event you should have a look at Intersection Observer (IO).
This was designed to solve problems like yours. And it is much more performant than listening to scroll events and then calculating the position yourself.
Of course you can continue using just scroll events, the official Polyfill from W3C uses scroll events to emulate IO for older browsers. Listening for scroll event and calculating position is not performant, especially if there are multiple elements. So if you care about user experience I really recommend using IO. Just wanted to add this answer to show what the modern solution for such a problem would be.
I took my time to create an example based on IO, this should get you started.
Basically I defined two thresholds: One for 20 and one for 90%. If the element is 90% in the viewport then it's save to assume it will cover the header. So I set the class for the header to the element that is 90% in view.
Second threshold is for 20%, here we have to check if the element comes from the top or from the bottom into view. If it's visible 20% from the top then it will overlap with the header.
Adjust these values and adapt the logic as you see.
Edit: Edited it according to your comment, please note that you may see the effect better if you remove the console.log from my code so they don't clutter up your view.
I added one div where the header doesn't change (the green one)
const sections = document.querySelectorAll('.menu');
const config = {
rootMargin: '0px',
threshold: [.2, .9]
};
const observer = new IntersectionObserver(function (entries, self) {
entries.forEach(entry => {
console.log(entry); // log the IO entry for demo purposes
console.log(entry.target); // here you have the Element itself.
// you can test for className here for example
if (entry.isIntersecting) {
var headerEl = document.querySelector('header');
if (entry.intersectionRatio > 0.9) {
//intersection ratio bigger than 90%
//-> set header according to target
headerEl.className=entry.target.dataset.header;
} else {
//-> check if element is coming from top or from bottom into view
if (entry.target.getBoundingClientRect().top < 0 ) {
headerEl.className=entry.target.dataset.header;
}
}
}
});
}, config);
sections.forEach(section => {
observer.observe(section);
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.g-100vh {
height: 100vh
}
header {
min-height: 50px;
position: fixed;
background-color: green;
width: 100%;
}
header.white-menu {
color: white;
background-color: black;
}
header.black-menu {
color: black;
background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<header>
<p>Header Content </p>
</header>
<div class="grid-30-span g-100vh menu" style="background-color:darkblue;" data-header="white-menu">
<img
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
data-src="/images/example_darkblue.jpg"
class="lazyload"
alt="<?php echo $title; ?>">
</div>
<div class="grid-30-span g-100vh no-menu" style="background-color:green;" data-header="black-menu">
<h1> Here no change happens</h1>
<p>it stays at whatever state it was before, depending on if you scrolled up or down </p>
</div>
<div class="grid-30-span g-100vh menu" style="background-color:lightgrey;" data-header="black-menu">
<img
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
data-src="/images/example_lightgrey.jpg"
class="lazyload"
alt="<?php echo $title; ?>">
</div>

The reason is that however the JQuery selector selects all elements with .dark classes, when you chain the .offset().top or .height() methods on it, it will only save the first one into the variable:
var toCross_position = $(".dark").offset().top;
var toCross_height = $(".dark").height();
You could map all positions and heights into arrays and then you should also make the
var toCross_position = $(".dark").offset().top;
var toCross_height = $(".dark").height();
// only the first div's pos and height:
console.log(toCross_height, toCross_position);
var positions = $('.dark').toArray().map(elem => $(elem).offset().top);
var heights = $('.dark').toArray().map(elem => $(elem).height());
console.log('all .dark positions:', positions);
console.log('all .dark heights:', heights);
.dark {
background-color: #222;
color: white;
margin-bottom: 2rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="dark">Dark</div>
<div class="dark">Dark</div>
<div class="dark">Dark</div>
<div class="dark">Dark</div>
check if your header crosses them by looping through those values.

Related

How to check if lowest part of div is visible in viewport?

What is the formula to check if the lowest part of the div is visible in the viewport?
It doesn't matter upper half is visible or gets hidden while scrolling the div
You can use IntersectionObserver to recognise if something is on screen. If you make a placeholder and magnetise it to the bottom of a parent div then you can make it possible.
But if you don't want to use IntersectionObserver API you can try getBoundingClientRect() + window.innerHeight like shown below:
const targetEl = document.querySelector('#target');
const windowsHeight = window.innerHeight;
// I heartily recommend to use some kind of throttling (lo-dash.throttle) here to reduce amount of callback executions
document.addEventListener('scroll', () => {
const bottom = targetEl.getBoundingClientRect().bottom;
if (windowsHeight > bottom) {
console.log('bottom is visible');
} else {
console.log('bottom is hidden');
}
})
/* All css are just for the demo, you only need a JS code */
body {
padding: 300px 20px;
}
#target {
height: 2000px;
width: 100%;
background-color: gray;
}
<div id="target">
content here
</div>

How to use IntersectionObserver API to make navBar sticky

I am making the clone of a webpage which is made in JS but I am developing it by HTML, CSS, JS. Its navBar looks like this . Here is the link if you want to experience yourself link.
So, I have tried to implement this using IntersectionObserver API as well as by using window.addEventListener(). I don't want to implement this by using scroll event Listener because it is too heavy for end user.
const intersectionCB = ([entry]) => {
const elem = entry.target;
if (!entry.isIntersecting) {
elem.classList.add('nav__2-sticky');
// observer.unobserve(navBar);
} else {
elem.classList.remove('nav__2-sticky');
}
};
const observer = new IntersectionObserver(intersectionCB, {
root: null,
threshold: 0
});
observer.observe(navBar);
In HTML file
<div class="nav__2">
<div class="row nav__2--content">
<div class="logo-container">
<img src="img/logo-black.png" alt="" class="logo" />
</div>
........
In SCSS file
.nav {
&__2 {
top: 8rem;
position: absolute;
left: 0;
width: 100%;
&-sticky {
position: fixed;
top: 0;
}
}
}
You might understand what is happening. When navBar gets out of the view, (navBar is positioned at 8rem from top!). I append nav__2-sticky class (which is positioned fixed at 0 from top) to appear on the screen. Due to which entry.isIntersecting becomes true and elem.classList.remove('nav__2-sticky'); is executed. As a result navBar again gets out of the view and again elem.classList.add('nav__2-sticky') is executed. This cycle of adding and removing classes due to entry.isIntersecting becoming True and False is creating a problem for me. This happens in such speed that it shows abnormal behaviour.
So, is there any proper solution for this? I would also like to hear other solutions that might work.
I used scroll event after all. Here is the code, I think I don't need to explain. You will get more detailed explanation here link
const initialCords = navBar.getBoundingClientRect();
document.addEventListener('scroll', () => {
if (window.scrollY > initialCords.top) {
navBar.classList.add('nav__2-sticky');
} else {
navBar.classList.remove('nav__2-sticky');
}
});
Another angle could be to run the intersection observer on an element that is out of view (below the bottom of the screen) and not only the navbar itself

How can I load a long list and stay/start on the bottom of the view without scrolling

We have a message view in our app where we on initial rendering load a list of messages which are then rendered, going from <div>Loading ....</div> to [<Message>,<Message>,...,<InputBox>] (pseudo-jsx). Upon loading, the view is extended to many times the screen length, so we need to scroll to the bottom onLoad(). This is bothersome :
lazy loading images in the older parts of the conversation won't work, as we "scroll past" them, triggering loading
there should be no need to do scrollTo(99999): we want to start a freshly loaded page on the bottom!
So how can I have the initial "scroll position" of a container be the bottom of the container? This seems like a quite basic thing.
The following contrived example is designed to show you one possible solution by emulating your scenario. If I have this wrong please correct me.
(React example linked at bottom)
Ten images are loaded into individual <div> elements. To emulate network/loading delay each <div><img></div> is loaded every 1/2 second. Notice that nothing is visible while this happens other than the "Loading..." placeholder. Five seconds later, after all are loaded, a custom event is fired to indicate the loading is complete. The very last image will be dark blue rather than the light blue of the others.
An event handler responds to the custom event by removing the "Loading..." indicator, scrolling to the bottom <div> and finally setting visibility of the entire section to visible.
Note the <div>s just appear and the <section> has been scrolled to the bottom. The bottom <div> is the dark blue one.
const container = document.querySelector('section');
const divsToAdd = 10
let divCounter = 0;
const interval = setInterval(addDiv, 500);
document.addEventListener('panelLoadComplete', () => {
document.querySelector('section span:first-child').remove();
document.querySelector('section div:last-child').scrollIntoView();
container.style.visibility = 'visible';
});
function addDiv() {
const div = document.createElement('div');
const img = document.createElement('img');
div.style.display = 'inherit';
div.appendChild(img);
container.appendChild(div);
if (divCounter === divsToAdd) { // is last - dark blue
img.src = "https://via.placeholder.com/100/0000ff"
} else {
img.src = "https://via.placeholder.com/100/0088ff"
}
if (++divCounter > divsToAdd) {
clearInterval(interval);
document.dispatchEvent(new CustomEvent('panelLoadComplete'));
}
}
section {
visibility: hidden;
}
section span:first-child {
visibility: visible;
}
section>div:first-child {
background-color: lightblue;
width: 100px;
height: 100px;
}
section>div:last-child {
background-color: darkblue;
width: 100px;
height: 100px;
}
<section>
<span>Loading... (patience: this takes ~5 seconds)</span>
</section>
Finally, a simple React version:
React Example StackBlitz

Pinning Elements with Debounced Scroll Event for Performance

What is the right way to smoothly pin an element according to scroll position?
I tried debouncing a scroll listener for performance but the pinning is not accurate. Even with debouncing set to 10ms it's not smooth and the element doesn't snap cleanly to its initial position.
var scrolling = false;
var stickPosY = 100;
var heights = [];
$(".element").each( function(index) {
heights[index] = $(".element[data-trigger=" + index + "]").offset().top;
});
function pin() {
if ( !$("#aside").hasClass("fixed") ) {
var stickyLeft = $("#aside").offset().left;
var stickyWidth = $("#aside").outerWidth();
var stickyTop = $("#aside").offset().top - stickPosY;
$("#aside").addClass("fixed");
$("#aside").css({"left": stickyLeft, "top": stickyTop, "width": stickyWidth});
}
}
function unpin() {
$("#aside").css({"left": "", "top": "", "width": ""});
$("#aside").removeClass("fixed")
}
$( window ).scroll( function() {
scrolling = true;
});
setInterval( function() {
if ( scrolling ) {
scrolling = false;
var y = window.scrollY;
console.log(y);
// PIN SIDEBAR
y > stickPosY ? pin() : unpin();
//TRIGGERS
for (var i=0; i < heights.length; i++) {
if (y >= heights[i]) {
$('.element[data-trigger="' + i + '"]').addClass("blue");
}
else {
$('.element[data-trigger="' + i + '"]').removeClass("blue");
}
}
}
}, 250 );
Here's my Pen
I tried to use scrollMagic for the project on a scene with a pin and additional triggers but the scrolling wasn't very smooth. So I'm trying to rebuild it with a stripped-down version and debounced listeners. Is this approach possible, or should I rather try to optimize my scrollMagic scene?
As James points out, you can just use position: sticky as one option, but that doesn't work in older browsers and its uses are limited to simpler situations in newer browsers, so I'll continue with the JS solution assuming you want to go that route.
There is a lot going on in your JS, and I think you are probably overcomplicating things, so I will give you a few basics to consider.
When you are toggling things based on scroll, either toggle inline styles or a class, but not both. I would recommend toggling a class because it allows you to have one function that can work on multiple screen sizes (i.e., you can use media queries to change the behavior of your toggled class based on screen size). Also it keeps all your styles in one place instead of having them split between your JS and your stylesheet.
Try to keep the work you're doing while scrolling as minimal as possible. For example, cache references to elements in variables outside your scroll function so you're not continually looking them up every time you scroll a pixel. Avoid loops inside scroll functions.
Using setInterval is not generally the recommended approach for increasing performance on scroll functions. All that is going to do is run a function every X amount of time, all the time, whether you're scrolling or not. What you really want to do is rate-limit your scroll function directly. That way, if you scroll a long ways real fast your function will only be called a fraction of the total times it would otherwise be called, but if you scroll a short distance slowly it will still be called a minimum number of times to keep things looking smooth, and if you don't scroll at all then you're not calling your function at all. Also, you probably want to throttle your function in this case, not debounce it.
Consider using the throttle function from Underscore.js or Lodash.js instead of inventing your own because those ones are highly performant and guaranteed to work across a wide variety of browsers.
Here is a simple example of sticking an element to the top of the screen on scroll, throttled with Lodash. I'm using a 25ms throttle, which is about the maximum amount I'd recommend for keeping things looking smooth where you won't really notice the delay in the element sticking/unsticking as you scroll past your threshold. You could go down to as little as 10ms.
$(function() {
$(window).on('scroll', _.throttle(toggleClass, 25));
const myThing = $('#my-thing');
const threshold = $('#dummy-1').height();
function toggleClass() {
const y = window.scrollY;
if (y > threshold) {
myThing.addClass('stuck')
} else {
myThing.removeClass('stuck');
}
}
});
#dummy-1 {
height: 150px;
background-color: steelblue;
}
#dummy-2 {
height: 150px;
background-color: gold;
}
#my-thing {
width: 300px;
height: 75px;
background-color: firebrick;
position: absolute;
top: 150px;
left: 0;
}
#my-thing.stuck {
position: fixed;
top: 0;
}
body {
margin: 0;
height: 2000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.0.0/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="dummy-1"></div>
<div id="dummy-2"></div>
<div id="my-thing"></div>
You could try fixed or sticky CSS positioning:
#element {
position: fixed;
top: 80px;
left: 10px;
}
Position: fixed would keep the element always at 80px from the top and 10px from the left edge regardless of scroll position.
#element{
position: sticky;
top: 0;
right: 0;
left: 0;
}
This is from a project of mine. The element is a nav bar. It sits below a header bar, so when you are at the top of the page, you see the header then the nav below it, and as you scroll down, the header moves off screen but the nav sticks at the top and is always visible.

How can I keep my scroll bar size(length) as stable?

I have a scenario here, there is a div parent element. it has the css value as overflow:auto. when the user keep add multiple element, the overflow works, but the size of the scroll bar going small. (that's common!)
What i looking is, i don't want to make my scroll bars height became small or big. i would like to keep my scrollbar height became static, whatever the length of elements appended.
So I trying to hide and show the nested elements under the parent. instead of resizing the scroll bar i re-sizing the content.
I don't have much idea about this. but nearly i tried to get the one what i look.
but not get the result.
here is my code :
var $newdiv = $('.div');
$col = $('<div />');
var container = $('#content');
var n = 0;
var child = $('.show');
var parent = container;
var add = function () {
for(var i=1; i<=10; i++){
$newdiv.clone().removeClass('div').addClass('show')
.find('span').append(n = n < 9 ? '0'+(++n) : ++n).end().clone().appendTo($col);
}
}
$col.appendTo(container);
$('button').click(function () { add()});
$(container).slimScroll({});
container.scroll(function(){
totalCH = 20*n;
parentH = parent.outerHeight();
required = Math.abs(parentH - totalCH);
scrolled = $(this).scrollTop();
requireToHide = scrolled/20;
hidableNo = Math.round(requireToHide);
if(scrolled > lastScrollTop) {
//while scroll down
//$(".div:lt("+hidableNo+ ")" ).addClass('hideIt');
//$(".div:gt("+(n-1)+ ")" ).addClass( "showIt");
} else {
//while scroll up
//$(".div:lt("+(n-1)+ ")" ).addClass("showIt");
//$(".div:gt("+hidableNo+ ")" ).addClass("hideIt");
}
lastScrollTop = scrolled;
});
var lastScrollTop = 0;
Try Online
Any one suggest me the correct approach.
Thanks in advance!
Please avoid plug-in and consider the support ie9
I'm going to refer to "scroller" as the draggable element in the scrollbar.
The scroller changing size is a standard UI representation that lets the user understand the amount of content in the scroll area; the goal of which is also to keep the rate of scrolling of the content to a standard flow rate.
For example, if you have a large amount of content in the scrollable area, dragging on a smaller scroller means that the content will flow at (about) the same speed as having less scrollable content (and a larger scroller).
Having a constant size scroller, will mean that a large amount of content would need to scroll much faster than a smaller amount of content.
To achieve what you want, I believe you're going to need an addon that takes the scroll speed/acceleration into account.
I use: jquery.mCustomscroll bar for custom scrolling behaviors.
http://manos.malihu.gr/jquery-custom-content-scroller/
It even has a setting to do exactly what you want:
autoDraggerLength: boolean
Enable or disable auto-adjusting scrollbar dragger length in relation
to scrolling amount (same behavior with browser’s native scrollbar).
Set autoDraggerLength: false when you want your scrollbar to (always)
have a fixed size.
You can solved with css
::-webkit-scrollbar{
width: 10px;
}
::-webkit-scrollbar-track{
-webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.3);
border: 1px solid black;
background: rgb(41,41,41);
border-radius: 10px;}
::-webkit-scrollbar-thumb{
border-radius:10px;
width: 8px;
height: 150px;
border: 1px solid black;
background: white;
}

Categories

Resources