How to use IntersectionObserver API to make navBar sticky - javascript

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

Related

(Resolved )Best way to remove Draggable React component from flow without jumping?

I have Note components that are rendered using notes.map(), each with position: static. The notes are draggable using react-draggable npm module.
The functionality I'm trying to achieve is when a note is deleted, to not affect the position of notes that have been dragged by the user. I've attempted to set position: absolute for notes that have been dragged. However, this causes the note to 'jump' in position once (happens when removed from flow).
-- Initial State:
-- After first drag attempt, test note jumps on top of other note:
-- Able to drag normally after first attempt:
I've included relevant code for Notes.jsx component:
function Note(props) {
const [dragDisabled, setDragDisabled] = useState(true);
const [beenDragged, setBeenDragged] = useState(props.beenDragged);
const [position, setPosition] = useState({ xPos: props.xPos, yPos: props.yPos });
// Drag Note Functions
function handleClick() {
setDragDisabled(prevValue => {
return !prevValue;
})
}
function firstDrag(event) {
if (!beenDragged) {
axios.post("api/note/beenDragged", {id: props.id})
.then(setBeenDragged(true));
}
}
function finishDrag(event, data) {
setPosition({ xPos: data.x, yPos: data.y });
}
useEffect(() => {
axios.post("/api/note/updateposition", {position, id: props.id });
}, [position]);
return <Draggable
disabled={dragDisabled}
onStart={firstDrag}
onStop={finishDrag}
defaultPosition={{ x: props.xPos, y: props.yPos }}
// position={location}
>
<div className='note' style={{position: beenDragged ? 'absolute' : 'static'}}>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleClick}>
{dragDisabled ? <LockIcon /> : <LockOpenIcon />}
</button>
<EditPopup title={props.title} content={props.content} editNote={editNote} />
<DeletePopup deleteNote={deleteNote} />
</div>
</Draggable>
}
and for my CSS styling
.note {
background: #fff;
/* background-image: url("https://www.transparenttextures.com/patterns/notebook-dark.png"); */
background-image: url("https://www.transparenttextures.com/patterns/lined-paper-2.png");
border-radius: 7px;
box-shadow: 0 2px 5px rgb(120, 150, 179);
padding: 10px;
width: 240px;
margin: 16px;
float: left;
}
Relevant code for how App.jsx renders the notes:
function App() {
const [notes, setNotes] = useState([]);
return (
<div id="bootstrap-override">
{notes.map((note) => {
return <Note
key={note._id}
id={note._id}
title={note.title}
content={note.content}
xPos={note.xPos}
yPos={note.yPos}
beenDragged={note.beenDragged}
deleteNote={deleteNote}
editNote={editNote}
/>
})}
</div>);
}
Any help is much appreciated, thank you!
I solved the issue not directly, but rather using a different approach to get the result I was looking for.
To summarize, I used position: absolute for all notes, and created them at random x,y coordinates within a boundary. This way deleted notes would not affect the position of existing notes.
Hope this helps anyone with similar issues!
When your draggables are position: static, you'll experience that screen jank when removing an element because the order of the elements in the document help define where your draggables live, regardless of their dragged state (which boils down to something like transform: translate(100px, 150px);.
The translate is like saying: "go over 100px and up 100px from wherever you live. When we delete the first element, everybody shifts. They can still translate(100px, 150px); but the point from which they originate will have changed.
Someone mentioned setting the draggable's to position: absolute and the parent as position: relative. That works, but might introduce headaches of it's own.
I will offer an innocent, perhaps naive, solution: hiding the element. A quick and dirty way: instead of removing the element from the DOM, you can set the element's visibility: none. You can even delete the item in the backend and offer a "restore note" feature in the UI since the draggable will still contain content.
And whenever you're hiding things, make sure you do so with accessibility in mind. Here's a good article on that.
I also made a derpy screencast: https://www.youtube.com/watch?v=Fr0PfT3Frzk&ab_channel=SamKendrick. It goes a little fast, but I begin by deleting the first element to illustrate your problem. Then I undo the delete and instead attach a visibility: hidden style to the element I want to delete.

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

Prevent JavaScript/jQuery from reacting to mouse from under a higher stacked div

I'm running this plugin in WordPress that makes pretty image maps with shapes over images. It uses JS/jQuery extensively. I've used a custom script to show/hide a div when I click a shape. This particular div is stacked above the div that contains the image map. It appears and disappears just fine, but when I mouse over it and/or click on it, for some weird reason the hovers and clicks get through the higher stacked div down to the map div while still working on the popover one. Here's a simplified version of how things are set up:
function toggle_visibility(id) {
var e = document.getElementById(id);
if (e.style.opacity == '1')
setTimeout(() => {
e.style.opacity = '0';
}, this.animationDelay + 2),
setTimeout(() => {
e.style.zIndex = '-30';
}, 400);
else
e.style.zIndex = '999999999',
setTimeout(() => {
e.style.opacity = '1';
}, this.animationDelay + 2);
}
.city {
position: relative;
}
#ipano {
position: fixed;
min-width: 90vw;
left: 5vw;
top: 50%;
transform: translateY(-50%);
opacity: 0;
z-index: -30;
}
<div class="city">
// map code
</div>
<div id="ipano">
//popup code
</div>
The map plugin allows me to assign onClick code to any shape defined, so I added this to make a click on the shape reveal the hidden div like so: toggle_visibility('ipano'); javascript:event.preventDeafult();.
But even if I remove my custom script for revealing the #ipano div, the problem still persists. You can see it in action here.
I think this is somehow due to the map script and how it handles the mapping of the overlay map regions (with an .svg), but I can't put my finger on it.
I've tried changing the divs' order, removing the preventDefault bit, and adding pointer-events: none; to some and all the map's elements, but that didn't help. How's that even possible?
Any suggestions in the right direction greatly appreciated, thank you!

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.

Chat type - scroll to bottom - angular 2

Need to scroll to bottom automatically once messages are being added in a chat.
Tried AfterViewChecked method of implementing this, as suggessted in
[angular2 scroll to bottom (chat style) - It works fine as it makes the scroll move to bottom. But when we try scrolling up as soon as we add a message in the chat, it is not allowing and it again pushes the view to the bottom of the chat
Please suggest a workaround on this.
The below code worked for me for my angular 4 chat application
My component.html
<div class="feedBody" #scrollMe (scroll)="onScroll()" >
<div class="feedList">
</div>
</div>
My component.css
.feedBody {
height: 235px;
overflow-y: auto;
}
My component.ts
scrollToBottom():void{
if (this.disableScrollDown) {
return
}
try {
this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
} catch(err) { }
}
onScroll(){
let element = this.myScrollContainer.nativeElement;
let atBottom = (element.scrollTop+200) >= (element.scrollHeight - element.offsetHeight);
if (atBottom) {
this.disableScrollDown = false
} else {
this.disableScrollDown = true
}
}
I was using element.scrollTop+200 because I wanted a behaviour where I should not compulsorily present at the bottom of the screen but can be a little top by 200px.
Hope it works for you.

Categories

Resources