Fixed Element on scroll issue - javascript

So, I'm trying to reproduce the main scrolling effect on this beautiful portfolio: http://melaniedaveid.com/
I've followed this tutorial on Codyhouse: http://codyhouse.co/gem/fixed-background-effect/
And came up with the following Javascript function.
$(window).scroll(function(){
var scrollTop = $(window).scrollTop(),
windowHeight = $(window).height(),
contentright1 = document.getElementById('contentright1');
function checkScroll(id) {
var offset = scrollTop - $(id).offset().top;
if (offset >= 0 && offset < windowHeight) {
$(id).addClass('fixed_content');
if ((scrollTop/2) <= windowHeight) {
$(id).removeClass('fixed_content');
}
}
else {
$(id).removeClass('fixed_content');
}
};
checkScroll(contentright1);
The fidex_content class apply the following CSS:
.fixed_content {
position: fixed;
top:0; }
As you can see, my main problem is that I can't manage to remove this class when I go back to the position of the element in the first place.

Related

On footer scroll keep it fixed

I have an issue i've been working on for a day or so, i'm making an online shop, where when you visit a specific item's page, you have a fixed [add to cart] button as a footer, and when i scroll to a specific point, it should become static, check this example here: https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_sticky_header
the only difference is that i need it to be a header, not a footer.
here's my jquery code:
const myFunction = () => {
let lastScrollTop = 0
$(window).scroll(() => {
const footerTop = $('.wrapper-footer')?.offset()?.top || null
const container = $('.wrapper-mobile-price')
const containerHeight = wrapperMobilePrice.height()
const bottomOfScreen = $(window).scrollTop() + $(window).innerHeight()
const st = $(window).scrollTop()
if (st > lastScrollTop) {
if (bottomOfScreen > footerTop + (containerHeight / 2)) {
container.css({position: 'static'})
}
} else {
if (bottomOfScreen + containerHeight < footerTop) {
container.css({position: 'fixed'})
}
}
lastScrollTop = st
})
}
please help if you have any solutions, thanks!
Have you tried using CSS sticky positioning? You can see an example here
You should specify in which position an element should be fixed.
When an element reaches that position it becomes fixed.
div.sticky {
position: -webkit-sticky; /* Safari */
position: sticky;
top: 500px;
}

debounce/throttle is ruining my fixed-header effect

I have written some javaScript so that my menu starts off as position: static but will become position: fixed and stay at the top of the screen whenever the user scrolls upwards but will disappear again whenever scrolling downwards. Because the menu has some content above it, once the user has scrolled to the very top, the menu becomes position: static again.
This code works ok but I am having a problem when adding debounce. I've read I need either throttling or debounce for performance. I have tried using both the Lodash _.debounce and _.throttle functions separately. I don't mind having some delay on the menu showing itself on scroll-up, but with a debounce the header has a delay when returning to position: static once the user has scrolled back to the top of the page. I have tried using the {'leading': true} option for the debounce and throttle function but it hasn't done much good.
If I set my wait/delay time too low, surely there is no point in even using debounce or throttle anymore? I do not want to sacrifice the performance of the site but have been asked to implement this effect.
var header = document.getElementById("fixed-header");
var offset = header.offsetTop;
var $header = $(header);
var headerHeight = parseInt($header.css("height"));
var total = headerHeight + offset;
var lastScrollTop = 0;
window.addEventListener("scroll", _.debounce(scrollHeader, 200, {
'leading': true
}));
function scrollHeader() {
var st = window.pageYOffset || document.documentElement.scrollTop;
if (st > lastScrollTop) {
// downscroll code
if (pageYOffset >= total) {
document.body.classList.add("fixed");
document.body.classList.add("is-hidden");
document.body.style.paddingTop = header.offsetHeight + "px";
header.style.transition = ".5s";
} else {
document.body.classList.remove("fixed");
document.body.classList.remove("is-hidden");
document.body.style.paddingTop = 0;
}
} else {
// upscroll code
if (pageYOffset >= offset) {
document.body.classList.add("fixed");
document.body.classList.remove("is-hidden");
document.body.style.paddingTop = header.offsetHeight + "px";
} else {
header.style.transition = "initial";
document.body.classList.remove("fixed");
document.body.style.paddingTop = 0;
}
}
lastScrollTop = st;
}

Get the visible height of a div with jQuery

I need to retrieve the visible height of a div within a scrollable area. I consider myself pretty decent with jQuery, but this is completely throwing me off.
Let's say I've got a red div within a black wrapper:
In the graphic above, the jQuery function would return 248, the visible portion of the div.
Once the user scrolls past the top of the div, as in the above graphic, it would report 296.
Now, once the user has scrolled past the div, it would again report 248.
Obviously my numbers aren't going to be as consistent and clear as they are in this demo, or I'd just hard code for those numbers.
I have a bit of a theory:
Get the height of the window
Get the height of the div
Get the initial offset of the div from the top of the window
Get the offset as the user scrolls.
If the offset is positive, it means the top of the div is still visible.
if it's negative, the top of the div has been eclipsed by the window. At this point, the div could either be taking up the whole height of the window, or the bottom of the div could be showing
If the bottom of the div is showing, figure out the gap between it and the bottom of the window.
It seems pretty simple, but I just can't wrap my head around it. I'll take another crack tomorrow morning; I just figured some of you geniuses might be able to help.
Thanks!
UPDATE: I figured this out on my own, but looks like one of the answers below is more elegant, so I'll be using that instead. For the curious, here's what I came up with:
$(document).ready(function() {
var windowHeight = $(window).height();
var overviewHeight = $("#overview").height();
var overviewStaticTop = $("#overview").offset().top;
var overviewScrollTop = overviewStaticTop - $(window).scrollTop();
var overviewStaticBottom = overviewStaticTop + $("#overview").height();
var overviewScrollBottom = windowHeight - (overviewStaticBottom - $(window).scrollTop());
var visibleArea;
if ((overviewHeight + overviewScrollTop) < windowHeight) {
// alert("bottom is showing!");
visibleArea = windowHeight - overviewScrollBottom;
// alert(visibleArea);
} else {
if (overviewScrollTop < 0) {
// alert("is full height");
visibleArea = windowHeight;
// alert(visibleArea);
} else {
// alert("top is showing");
visibleArea = windowHeight - overviewScrollTop;
// alert(visibleArea);
}
}
});
Calculate the amount of px an element (height) is in viewport
Fiddle demo
This tiny function will return the amount of px an element is visible in the (vertical) Viewport:
function inViewport($el) {
var elH = $el.outerHeight(),
H = $(window).height(),
r = $el[0].getBoundingClientRect(), t=r.top, b=r.bottom;
return Math.max(0, t>0? Math.min(elH, H-t) : Math.min(b, H));
}
Use like:
$(window).on("scroll resize", function(){
console.log( inViewport($('#elementID')) ); // n px in viewport
});
that's it.
jQuery .inViewport() Plugin
jsFiddle demo
from the above you can extract the logic and create a plugin like this one:
/**
* inViewport jQuery plugin by Roko C.B.
* http://stackoverflow.com/a/26831113/383904
* Returns a callback function with an argument holding
* the current amount of px an element is visible in viewport
* (The min returned value is 0 (element outside of viewport)
*/
;(function($, win) {
$.fn.inViewport = function(cb) {
return this.each(function(i,el) {
function visPx(){
var elH = $(el).outerHeight(),
H = $(win).height(),
r = el.getBoundingClientRect(), t=r.top, b=r.bottom;
return cb.call(el, Math.max(0, t>0? Math.min(elH, H-t) : Math.min(b, H)));
}
visPx();
$(win).on("resize scroll", visPx);
});
};
}(jQuery, window));
Use like:
$("selector").inViewport(function(px) {
console.log( px ); // `px` represents the amount of visible height
if(px > 0) {
// do this if element enters the viewport // px > 0
}else{
// do that if element exits the viewport // px = 0
}
}); // Here you can chain other jQuery methods to your selector
your selectors will dynamically listen to window scroll and resize but also return the initial value on DOM ready trough the first callback function argument px.
Here is a quick and dirty concept. It basically compares the offset().top of the element to the top of the window, and the offset().top + height() to the bottom of the window:
function getVisible() {
var $el = $('#foo'),
scrollTop = $(this).scrollTop(),
scrollBot = scrollTop + $(this).height(),
elTop = $el.offset().top,
elBottom = elTop + $el.outerHeight(),
visibleTop = elTop < scrollTop ? scrollTop : elTop,
visibleBottom = elBottom > scrollBot ? scrollBot : elBottom;
$('#notification').text(`Visible height of div: ${visibleBottom - visibleTop}px`);
}
$(window).on('scroll resize', getVisible).trigger('scroll');
html,
body {
margin: 100px 0;
}
#foo {
height: 1000px;
background-color: #C00;
width: 200px;
margin: 0 auto;
}
#notification {
position: fixed;
top: 0;
left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<div id="foo"></div>
<div id="notification"></div>
The logic can be made more succinct if necessary, I've just declared separate variables for this example to make the calculation as clear as I can.
Here is a version of Rory's approach above, except written to function as a jQuery plugin. It may have more general applicability in that format. Great answer, Rory - thanks!
$.fn.visibleHeight = function() {
var elBottom, elTop, scrollBot, scrollTop, visibleBottom, visibleTop;
scrollTop = $(window).scrollTop();
scrollBot = scrollTop + $(window).height();
elTop = this.offset().top;
elBottom = elTop + this.outerHeight();
visibleTop = elTop < scrollTop ? scrollTop : elTop;
visibleBottom = elBottom > scrollBot ? scrollBot : elBottom;
return visibleBottom - visibleTop
}
Can be called with the following:
$("#myDiv").visibleHeight();
jsFiddle
Here is the improved code for jquery function visibleHeight: $("#myDiv").visibleHeight();
$.fn.visibleHeight = function() {
var elBottom, elTop, scrollBot, scrollTop, visibleBottom, visibleTop, height;
scrollTop = $(window).scrollTop();
scrollBot = scrollTop + $(window).height();
elTop = this.offset().top;
elBottom = elTop + this.outerHeight();
visibleTop = elTop < scrollTop ? scrollTop : elTop;
visibleBottom = elBottom > scrollBot ? scrollBot : elBottom;
height = visibleBottom - visibleTop;
return height > 0 ? height : 0;
}

calculate centered absolute div inside relative

I'm using 2 Javscript methods to position a hovering button inside a static element on my page. The button that is centered is inputted inside the first element and uses position absolute. The code I'm using to get the parent element measurements:
// calculate if the element is in the visible window
function elementVisibleRect(element) {
element = $(element);
var rect = {
top: Math.round(element.offset().top),
left: Math.round(element.offset().left),
width: Math.round(element.outerWidth()),
height: Math.round(element.outerHeight())
};
var scrollTop = $(window).scrollTop();
var windowHeight = $(window).height();
var scrollBottom = scrollTop + windowHeight;
var elementBottom = Math.round(rect.height + rect.top);
if (scrollTop < rect.top && scrollBottom > elementBottom) {
return rect;
}
if (scrollTop > rect.top) {
rect.top = scrollTop;
}
if (scrollBottom < elementBottom) {
rect.height = scrollBottom - rect.top;
} else {
rect.height = windowHeight - (scrollBottom - elementBottom);
}
return rect;
}
and for using this information and centering the button inside
// center the element based on visible screen-frame
function elementPosition (element) {
var visibleRect = elementVisibleRect(element);
$('.editHoverButton').css({
top: visibleRect.top + ((visibleRect.height / 2) - ($('.editHoverButton').outerHeight() / 2)),
left: visibleRect.left + (visibleRect.width / 2) - ($('.editHoverButton').outerWidth() / 2)
});
}
Now my problem is that a third party library requires the parent DIV to change position from the browser default "static" to "relative" which breaks my calculations in the second function.
It might be late, but no matter what I try I can't seem to figure out how to get this working for when the parent element has position set to relative. I can't seem to get the maths quite right, and my head is beginning to hurt. Any suggestions?
EDIT - ADDED JSFIDDLE
http://jsfiddle.net/RhTY6/
Elements with absolute positioning are removed from the natural flow (e.g. they don't leave a space where they were) and positioned relative to their first parent with non-static positioning. Since the positioning of the right-hand box is relative (not static), you can position the button with top: 50%; and left: 50%;. This will make the top-left corner at the center of the parent. Then all you have to do is subtract half the element's height and width from the position, using margin-top and margin-left. This is much simpler than what you were doing, as you can see below:
JavaScript:
function elementPosition() {
$('.editHoverButton').css('margin-top', 0 - $('.editHoverButton').outerHeight() / 2);
$('.editHoverButton').css('margin-left', 0 - $('.editHoverButton').outerWidth() / 2);
};
CSS:
.editHoverButton {
position: absolute;
z-index: 99;
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
background-color: #00bb00;
top: 50%;
left: 50%;
}
Nothing else has to change except to remove this from the elementPosition() function.
DEMO (Notice that the left one no longer works. This is because it is positioned static.)
EDIT--Using the same basic idea, this method should work:
The problem is that you have take the top and left positions of the element when defining rect. on the positioning calculations. Changing those to 0 (not the best method, but it works) fixes the problem for relative elements.
DEMO (Notice that the left one now does work. This is because it is positioned at 0,0 anyway.)
EDIT--This will work when the page scrolls:
This sets the container in a variable so that when the page scrolls, it can be repositioned automatically.
DEMO
EDIT: made it worked with your CSS and HTML (relative and absolute positioning) by altering the Script only.
The horizontal axis calcs were completely missing (I've applied the same calcs you applied to the vertical axis).
I've added some data and a ruler to help you finish the job: as you can see, it is (and it was, in your original fiddle) not perfectly centered (obviously you need to look at it when the container is smaller than the viewport), but this will be easy to work out.
Running Demo
Try to resize the fiddle window and to scroll both vertically and horizontally to see it works.
function elementVisibleRect(element) {
$("#data").html("");
element = $(element);
var rect = {
top: Math.round(element.offset().top),
left: Math.round(element.offset().left),
width: Math.round(element.outerWidth()),
height: Math.round(element.outerHeight())
};
var scrollTop = $(window).scrollTop();
var windowHeight = $(window).height();
var scrollBottom = scrollTop + windowHeight;
var elementBottom = Math.round(rect.height + rect.top);
var scrollLeft = $(window).scrollLeft();
var windowWidth = $(window).width();
var scrollRight = scrollLeft + windowWidth;
var elementRight = Math.round(rect.width + rect.left);
addData("rect.top", rect.top);
addData("rect.left", rect.left);
addData("rect.width", rect.width);
addData("rect.height", rect.height);
addData("scrollTop", scrollTop);
addData("windowHeight", windowHeight);
addData("scrollBottom", scrollBottom);
addData("elementBottom", elementBottom);
addData("scrollLeft", scrollLeft);
addData("windowWidth", windowWidth);
addData("scrollRight", scrollRight);
addData("elementRight", elementRight);
if (rect.top < scrollTop) {
rect.top = scrollTop;
}
if (scrollBottom < rect.top < scrollTop) {
rect.top = scrollTop;
}
if (scrollBottom < elementBottom) {
rect.height = scrollBottom - rect.top;
} else {
rect.height = windowHeight - (scrollBottom - elementBottom);
}
if (rect.left < scrollLeft) {
rect.left = scrollLeft;
}
if (scrollRight < rect.left < scrollLeft) {
rect.left = scrollLeft;
}
if (scrollRight < elementRight) {
rect.width = scrollRight - rect.left;
} else {
rect.width = windowWidth - (scrollRight - elementRight);
}
return rect;
}

Jitters when setting scroll on window

EDIT:
I figured out the cause of the issue. When you scroll with the scrollbar, and then programmatically set the scroll, the scrollbar moves, which causes the browser to recalculate the scroll position based on the position of the mouse relative to the new scrollbar position. Now I'm looking for a solution. Here is a Youtube video of the problem using code from one of the answers below.
ORIGINAL POST:
Im trying to achieve an effect where the window scrolls in increments equal to the size of elements in a grid, so that the scrolling looks smooth. The elements are 40px square, with 4px of padding, so I'm trying to scroll the page in increments of 44px. I've written some code, and it works great when the page scrolls vertically. I've got the exact same operation (or so I think) set up for horizontal scrolling, but when I scroll horizontally, the page jitters back and forth. I've tried debugging it, and it seems like the window scroll left gets set to some seemingly random value every second or third time the scroll handler is invoked.
Here is the code (and a fiddle):
$(function(){
// Here is the scroll function in question
$(window).scroll(function(){
var lastScrollTop = $(window).scrollTop();
var lastScrollLeft = $(window).scrollLeft();
return function(){
var scrollTop = $(this).scrollTop();
var scrollLeft = $(this).scrollLeft();
var yDir = scrollTop - lastScrollTop > 0 ? 1 : -1;
var xDir = scrollLeft - lastScrollLeft > 0 ? 1 : -1;
$(this).scrollTop(~yDir ? scrollTop + ((44 - ((scrollTop + 44) % 44))) : scrollTop - (scrollTop % 44));
$(this).scrollLeft(~xDir ? scrollLeft + ((44 - ((scrollLeft + 44) % 44))) : scrollLeft - (scrollLeft % 44));
scrollTop = lastScrollTop = $(this).scrollTop();
scrollLeft = lastScrollLeft = $(this).scrollLeft();
}
}());
});
EDIT:
I'm testing this in Chrome 26.0.1410.65 btw.
EDIT:
The problem occurs with vertical scrolling too, but only when using the scrollbar, not the mousewheel.
There are cases where scrollTop - lastScrollTop and scrollLeft - lastScrollLeft are zero and is causing problems in your logic.
See these two lines
var yDir = scrollTop - lastScrollTop > 0 ? 1 : -1;
var xDir = scrollLeft - lastScrollLeft > 0 ? 1 : -1;
Say the difference is > 0, your xDir variable will have 1 and you set scrollLeft to scrollTop + ((44 - ((scrollTop + 44) % 44)).
Next iteration difference is 0, your xDir variable will have -1 and you will now use scrollLeft - (scrollLeft % 44)). These two equations do not evaluate to the same even though the scroll position didn't changed.
Try this.
$(function(){
// Here is the scroll function in question
$(window).scroll(function(){
var lastScrollLeft = $(window).scrollLeft();
var lastScrollTop = $(window).scrollTop();
return function(){
var scrollLeft = $(this).scrollLeft();
var scrollTop = $(this).scrollTop();
if (lastScrollLeft !== scrollLeft) {
lastScrollLeft = scrollLeft - (scrollLeft % 44);
$(this).scrollLeft(lastScrollLeft);
}
if (lastScrollTop !== scrollTop) {
lastScrollTop = scrollTop - (scrollTop % 44);
$(this).scrollTop(lastScrollTop);
}
}
}());
// Generates the grid, ignore this part
for(var i=0;i<50;i++){
var row = $('<div></div>').addClass('row').appendTo('body');
for(var j=0; j<50; j++)
$('<span></span>').appendTo(row).addClass('cell').text(50*i+j);
}
});
So, based on the cause of the issue as stated at the top of the question, I don't think there is a direct solution to this issue using the method in the code. The solution I ended up using was to implement a sort of virtual scrolling. I put the element to be scrolled into a 'viewport' div with a fixed position filling the window area. Then I expanded the window size using a 'scrollTarget' div, and scrolled the viewport in increments of 44 based on the scroll position of the window. See this fiddle.
Javascript
$(function(){
// Here is the scroll function in question
$(this).scroll(function(){
var scrollTop = $(this).scrollTop();
var scrollLeft = $(this).scrollLeft();
$('#viewport').scrollTop(scrollTop - (scrollTop % 44));
$('#viewport').scrollLeft(scrollLeft - (scrollLeft % 44));
});
// Generates the grid, ignore this part
for(var i=0;i<50;i++){
var row = $('<div></div>').addClass('row').appendTo('#viewport');
for(var j=0; j<50; j++)
$('<span></span>').appendTo(row).addClass('cell').text(50*i+j);
}
$('#scrollTarget').height((50 * 44) + 48)
.width((50 * 44) + 48);
});
HTML
<div id="viewport"></div>
<div id="scrollTarget"></div>
CSS
#viewport{
position:fixed;
height:100%;
width:100%;
overflow:hidden;
}
#scrollTarget{
position:absolute;
}
.row{
height:44px;
width:2204px;
}
.row:first-child{
height:40px;
}
.cell{
display:inline-block;
height:40px;
width:40px;
margin-top:4px;
margin-right:4px;
background-color:#859DE1;
color:#0F1219;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.row:first-child .cell{
margin-top:0px;
}

Categories