Multiple sticky sections with horizontal scrolling - javascript

I'm trying to make a page with multiple sticky sections with horizontal scrolling (so when you're scrolling vertically as normal, you're forced to go through the horizontal gallery)
I'm referencing this codepen (https://codepen.io/johnhubler/pen/RwoPRBG) as my JS knowledge is very poor. But, as you can see in the codepen, it is only working in the first sticky section, and the second one stays still.
var windowWidth = window.innerWidth;
var horLength = document.querySelector(".element-wrapper").scrollWidth;
var horLength2 = document.querySelector(".element-wrapper2").scrollWidth;
var distFromTop = document.querySelector(".horizontal-section").offsetTop;
var distFromTop2 = document.querySelector(".horizontal-section2").offsetTop;
var scrollDistance = distFromTop + horLength - windowWidth;
var scrollDistance2 = distFromTop2 + horLength2 - windowWidth;
document.querySelector(".horizontal-section").style.height = horLength + "px";
document.querySelector(".horizontal-section2").style.height = horLength2 + "px";
window.onscroll = function(){
var scrollTop = window.pageYOffset;
if (scrollTop >= distFromTop && scrollTop <= scrollDistance) {
document.querySelector(".element-wrapper").style.transform = "translateX(-"+(scrollTop - distFromTop)+"px)";
}
if (scrollTop >= distFromTop2 && scrollTop <= scrollDistance2) {
document.querySelector(".element-wrapper2").style.transform = "translateX(-"+(scrollTop - distFromTop2)+"px)";
}
}
I'm planning to add around 4 of the same sticky sections, so I'd like to know how to make it work in all of them. If there is a better alternative/resource/etc.(if possible, vanilla JS or something very easy to follow) please let me know.
Thank you

I made an optimized and working version of your code.
This array lists the classes of packaging elements. This way you can add as many galleries as you want by simply adding a new class to the array.
var array = ['.horizontal-section', '.horizontal-section2'];
Example:
var array = ['.horizontal-section', '.horizontal-section2'];
window.onscroll = function () {
var windowWidth = window.innerWidth;
var scrollTop = window.pageYOffset;
array.forEach(el => {
var wrap = document.querySelector(el);
var elWrap = wrap.querySelector(".element-wrapper");
var horLength = elWrap.scrollWidth;
var distFromTop = wrap.offsetTop;
var scrollDistance = distFromTop + horLength - windowWidth;
wrap.style.height = horLength + "px";
if (scrollTop >= distFromTop && scrollTop <= scrollDistance) {
elWrap.style.transform = "translateX(-" + (scrollTop - distFromTop) + "px)";
}
});
}
* {
box-sizing: border-box;
}
body {
padding: 0;
margin: 0;
}
.bumper {
width: 100%;
height: 1800px;
background-color: #f3f3f3;
}
.horizontal-section,
.horizontal-section2 {
padding: 100px 0;
background-color: pink;
}
.sticky-wrapper,
.sticky-wrapper2 {
position: sticky;
top: 100px;
width: 100%;
overflow: hidden;
}
.element-wrapper,
.element-wrapper2 {
position: relative;
display: flex;
}
.element {
width: 500px;
height: 400px;
background-color: purple;
margin: 0 20px 0 0;
flex-shrink: 0;
}
<div class="bumper"></div>
<div class="horizontal-section">
<div class="sticky-wrapper">
<div class="element-wrapper">
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>
</div>
</div>
</div>
<div class="bumper"></div>
<div class="horizontal-section2">
<div class="sticky-wrapper">
<div class="element-wrapper">
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>
</div>
</div>
</div>
<div class="bumper"></div>

Related

How detect which child element is visible after scrolling the parent div?

I would like to emulate something like "current page" using divs (like a PDF reader)
document.addEventListener("DOMContentLoaded", function(event) {
var container = document.getElementById("container");
container.onscroll = function() {
let position = container.scrollTop;
let divs = document.querySelectorAll('.page');
for (div of divs) {
//???
}
}
});
#container {
width: 400px;
height: 600px;
overflow: auto;
}
.page {
width: 400px;
}
.red {
background-color: red;
height: 600px;
}
.blue {
background-color: blue;
height: 400px;
}
Current page: <span id="page-counter">1</span>
<div id='container'>
<div id="div-1" class="page red"></div>
<div id="div-2" class="page blue"></div>
<div id="div-3" class="page red"></div>
<div id="div-4" class="page blue"></div>
</div>
So, I would like to know the best way to, for example, change span page-counter text to "3" when the third div "appears".
Something like this: https://i.imgur.com/rXQ2Bw8.png
Thanks in advance
Celso
Since this question never tagged jQuery, here's a pure Javascript solution that simulates the behavior you're looking for to the best of my knowledge. The solution calculates the amount of pixels of each child element currently visible within the container. If the amount is bigger or equal to half the size of the container, it assumes this is the page your visitor is looking at.
function getVisibleHeight(element){
const container = document.getElementById("container");
let scrollTop = container.scrollTop;
let scrollBot = scrollTop + container.clientHeight;
let containerRect = container.getBoundingClientRect();
let eleRect = element.getBoundingClientRect();
let rect = {};
rect.top = eleRect.top - containerRect.top,
rect.right = eleRect.right - containerRect.right,
rect.bottom = eleRect.bottom - containerRect.bottom,
rect.left = eleRect.left - containerRect.left;
let eleTop = rect.top + scrollTop;
let eleBot = eleTop + element.offsetHeight;
let visibleTop = eleTop < scrollTop ? scrollTop : eleTop;
let visibleBot = eleBot > scrollBot ? scrollBot : eleBot;
return visibleBot - visibleTop;
}
document.addEventListener("DOMContentLoaded", function(event) {
const container = document.getElementById("container");
const divs = document.querySelectorAll('.page');
container.addEventListener("scroll", () => {
for(let i=0; i<divs.length; i++){
const containerHeight = container.clientHeight;
// Gets the amount of pixels currently visible within the container
let visiblePageHeight = getVisibleHeight(divs[i]);
// If the amount of visible pixels is bigger or equal to half the container size, set page
if(visiblePageHeight >= containerHeight / 2){
document.getElementById('page-counter').innerText = i+1;
}
}
}, false);
});
#container {
width: 400px;
height: 300px;
overflow: auto;
}
.page {
width: 380px;
}
.red {
background-color: red;
height: 300px;
}
.blue {
background-color: blue;
height: 200px;
}
Current page: <span id="page-counter">1</span>
<div id='container'>
<div id="div-1" class="page red"></div>
<div id="div-2" class="page blue"></div>
<div id="div-3" class="page red"></div>
<div id="div-4" class="page blue"></div>
</div>
The general approach here would be to write a function that determines if a given HTML element is in the viewport. You could run the check as the user scrolls. See the snippet below for an example with jQuery. I'm not necessarily saying this is the best way to do this, but it seems to be working. Start scrolling to see the IDs appear.
function isInViewPort(element) {
// Function will determine if any part of the element is in the viewport.
let $el = $("#" + element);
let windowScrollTop = $(window).scrollTop();
let windowHeight = $(window).height();
let windowBottom = windowScrollTop + windowHeight;
let elementTop = $el.offset().top;
let elementOuterHeight = $el.outerHeight();
let elementBottom = elementTop + elementOuterHeight;
let isAboveViewPort = elementBottom < windowScrollTop;
let isBelowViewPort = windowBottom < elementTop;
return !(isAboveViewPort || isBelowViewPort);
}
let currentDiv;
$("#container").on("scroll", function() {
$("#container").find("div").each(function() {
if (isInViewPort(this.id) && currentDiv !== this.id) {
$("#page").html("Current ID is " + this.id)
currentDiv = this.id;
}
});
});
#container {
overflow: auto;
height: 300px;
}
.red {
background-color: red;
height: 600px;
}
.blue {
background-color: blue;
height: 400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<span id="page"></span>
<div id='container'>
<div id="div-1" class="page red"></div>
<div id="div-2" class="page blue"></div>
<div id="div-3" class="page red"></div>
<div id="div-4" class="page blue"></div>
</div>
you can use the is visible feature in jQuery. Just give each div a unique ID or class.
if( $("#uniqueIdHere").is(':visible'))
$(".page3Selector").addClass('active');
and then to remove the active class you could pair it up with an else statement to remove the class of the inactive div.

Padding aspect ratio hack, but based on height

Most of you know a trick that makes element scale at a fixed aspect ratio, for example:
<div style="width:100%;position:relative">
<div style="padding-bottom:100%;background:tomato;height:0;">square (1:1)</div>
</div>
(jsfiddle https://jsfiddle.net/0dda7939/ )
I want the same thing, but based on height not based on width.
<div style="height:100%;position:relative">
<div style="??? background:tomato;">square (1:1)</div>
</div>
Is there any way to do this without JS and without using VW/VH units (since parent container might not always be window)?
With JS it can be done easily. The problem with doing by CSS is applying width equal to the height. If you are comfortable to use SASS, then you can do that. Otherwise, JS (jQuery) solution is given below:
HTML:
<div class="parent">
<div class="child">I am a square (1:1)</div>
</div>
CSS:
.parent {
border: 1px solid black;
width:auto;
height: 200px;
position:relative
}
.child {
background:tomato;
height:inherit;
}
JS:
let parentWidth = $('.parent').width();
let parentHeight = $('.parent').height();
let childWidth = $('.child').width();
let childHeight = $('.child').height();
let minSize = 0;
$('.child').width(childHeight);
$(window).on('resize', function () {
parentWidth = $('.parent').width();
parentHeight = $('.parent').height();
childWidth = $('.child').width();
childHeight = $('.child').height();
minSize = Math.min(parentWidth, parentHeight);
if (childWidth >= parentWidth || childHeight >= parentHeight) {
if (childWidth >= parentWidth && childHeight >= parentHeight) {
$('.child').height(minSize);
$('.child').width(minSize);
} else if (childWidth >= parentWidth) {
$('.child').width(parentWidth);
$('.child').height(parentWidth);
} else {
$('.child').height(parentHeight);
$('.child').width(parentHeight);
}
} else {
$('.child').height(minSize);
$('.child').width(minSize);
}
});
Try adding this code::
<div style="display: table; width: 100%; height: 100%; text-align: center;">
<div style="display: table-cell; vertical-align: middle; background:tomato;">square (1:1)</div>
</div>

Sticky sidebar flickers during scroll

I created this sidebar which sticks when the bottom of the div reaches it's bottom. However, it seems to flicker when I scroll. Could you help what am I doing wrong?
HTML
<div id="header"></div>
<div id="stickymain">
<div class="side" id="stickyside">
<p>
This is the best we could do and there's nothing more one could expect from here to carry from onwards. I think there's nothing better too.
</p>
<p>
This is the best we could do and there's nothing more one could expect from here to carry from onwards. I think there's nothing better too. This is the best we could do and there's nothing more one could expect from here to carry from onwards. I think there's nothing better too. This is the best we could do and there's nothing more one could expect from here to carry from onwards. I think there's nothing better too.11
</p>
</div>
<div class="main"></div>
<div class="main"></div>
<div class="main"></div>
<div class="main"></div>
<div class="main"></div>
</div>
<div id="footer"></div>
<script>
CSS
body {
margin: 0;
padding: 10px;
}
#header {
height: 100px;
margin: 0 0 10px;
background: red;
}
#content {
position: relative;
float: left;
width: 100%;
height: auto;
margin: 0 -110px 0 0;
}
.side {
float: right;
width: 100px;
/* min-height: 500px; */
margin: 0 0 0 10px;
background: linear-gradient(red, yellow);
}
.main {
height: 600px;
margin: 0 110px 10px 0;
background: lightgray;
}
#footer {
clear: both;
height: 100px;
background: orange;
}
JS
jQuery(document).ready(function() {
jQuery.fn.stickyTopBottom = function(){
var options = {
container: jQuery('#stickymain'),
top_offset: 0,
bottom_offset: 0
};
console.log(options);
let jQueryel = jQuery(this)
let container_top = options.container.offset().top
let element_top = jQueryel.offset().top
let viewport_height = jQuery(window).height()
jQuery(window).on('resize', function(){
viewport_height = jQuery(window).height()
});
let current_translate = 0
let last_viewport_top = document.documentElement.scrollTop || document.body.scrollTop
jQuery(window).scroll(function(){
var viewport_top = document.documentElement.scrollTop || document.body.scrollTop
let viewport_bottom = viewport_top + viewport_height
let effective_viewport_top = viewport_top + options.top_offset
let effective_viewport_bottom = viewport_bottom - options.bottom_offset
let element_height = jQueryel.height()
let is_scrolling_up = viewport_top < last_viewport_top
let element_fits_in_viewport = element_height < viewport_height
let new_translation = null
if (is_scrolling_up){
if (effective_viewport_top < container_top)
new_translation = 0
else if (effective_viewport_top < element_top + current_translate)
new_translation = effective_viewport_top - element_top
}else if (element_fits_in_viewport){
if (effective_viewport_top > element_top + current_translate)
new_translation = effective_viewport_top - element_top
}else {
let container_bottom = container_top + options.container.height()
if (effective_viewport_bottom > container_bottom)
new_translation = container_bottom - (element_top + element_height)
else if (effective_viewport_bottom > element_top + element_height + current_translate)
new_translation = effective_viewport_bottom - (element_top + element_height)
}
if (new_translation != null){
current_translate = new_translation;
console.log('i am here at css');
jQueryel.css('transform', ('translate(0, '+current_translate+'px)'));
}
last_viewport_top = viewport_top
});
}
jQuery('#stickyside').stickyTopBottom();
});
Except for the flickering issue when I scroll, everything else is working just the way I want. I'm on Mac using Chrome, Firefox and Safari.
CodePen Demo
Instead of using only javascript to do all the work, you can use css properties to help you.
You can just change the position property of your side bar when reaching a certain point of the viewport:
$(window).scroll(function() {
var sTop = $(this).scrollTop();
var footerTop = $('#footer').offset().top;
var sideHeight = $('.side').height();
var headerHeight = $('#header').height();
if(sTop > headerHeight + (sideHeight/2)) {
$('.side').css({
position:'fixed',
bottom:'10px'
});
} else {
$('.side').css({
position:'absolute',
bottom:'inherit'
});
}
});
See this PEN
I hope this one it's ok! :)

Freeze selected item when scrolling

I have a requirement, where I need to freeze the selected item from list of items in a container to top, when the selected item is in top fold of the container. and when the selected item is in bottom fold of the container, I need to stick it to the bottom.
If the selected item is in visible fold, nothing should happen. I mean the selected item should be in normal flow with other adjacent items.
I somehow managed to solve this to some extent. But when I scroll up, when the selected item is sticked above of the container, the selected item is hiding. This behavior is happening even when I scroll down, when the selected item is sticked to the bottom of the container.
Here is the Fiddle
$('.item').click(function () {
$('.item').removeClass('select').removeClass('pAbsolute');
$(this).addClass('select');
});
$('.parent').scroll(function () {
var $selected = $('.item.select');
var cTop = $selected.offset().top;
var cHeight = $selected.height();
var pHeight = $(this).height();
if (cTop < 0) {
$selected.css({
'top': $(this).scrollTop(),
'bottom': ''
}).addClass('pAbsolute');
} else if (cTop > pHeight - cHeight) {
$selected.css({
'bottom': -$(this).scrollTop(),
'top': ''
}).addClass('pAbsolute');
} else {
$selected.css({
'top': '',
'bottom': ''
}).removeClass('pAbsolute');
}
});
You have to use a consistent value to keep the initial offset relative to the container when you select it.
Then, calculate the offset and scroll value,
If cTop < 0, which means its top out of box, stick to top.
If cTop + cHeight > pHeight, which means its view is out of bottom block, set to bottom.
Else stay in position.
Edit:
When selecting a new Item, as the previous item may have .pAbsolute attr, the relative position of current item may change, but we can get the offset change by track the offset before and after those class add/remove actions.
Then we can add the missing height by change the scrollTop of the container manually.
var offset;
$('.item').click(function () {
// This is the offset in container before class change.
offset = this.offsetTop;
$('.item').removeClass('select').removeClass('pAbsolute');
$(this).addClass('select');
// Calculate the difference
var distortion = offset - this.offsetTop;
// Remove the distortion by manual scroll.
var $parent = $(this).parent();
$parent.scrollTop($parent.scrollTop() - distortion);
offset = this.offsetTop;
});
$('.parent').scroll(function () {
var $selected = $('.item.select');
var cTop = offset - $(this).scrollTop();
var cHeight = $selected.height();
var pHeight = $(this).height();
if (cTop < 0) {
$selected.css({
'top': $(this).scrollTop(),
'bottom': ''
}).addClass('pAbsolute');
} else if (cTop + cHeight > pHeight) {
$selected.css({
'bottom': -$(this).scrollTop(),
'top': ''
}).addClass('pAbsolute');
} else {
$selected.css({
'top': '',
'bottom': ''
}).removeClass('pAbsolute');
}
});
body, html {
padding: 0;
margin: 0;
}
.parent {
overflow: auto;
height: 200px;
position: relative;
}
.item {
padding: 10px 15px;
background-color: tomato;
width: 100%;
}
.item.select {
background-color: beige;
}
.pAbsolute {
position: absolute;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="parent">
<div class="item select">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
<div class="item">10</div>
<div class="item">11</div>
<div class="item">12</div>
<div class="item">13</div>
<div class="item">14</div>
<div class="item">15</div>
<div class="item">16</div>
<div class="item">17</div>
<div class="item">18</div>
<div class="item">19</div>
<div class="item">20</div>
<div class="item">21</div>
<div class="item">22</div>
<div class="item">23</div>
<div class="item">24</div>
<div class="item">25</div>
<div class="item">26</div>
<div class="item">27</div>
<div class="item">28</div>
<div class="item">29</div>
<div class="item">30</div>
<div class="item">31</div>
<div class="item">32</div>
<div class="item">33</div>
<div class="item">34</div>
<div class="item">35</div>
<div class="item">36</div>
<div class="item">37</div>
<div class="item">38</div>
<div class="item">39</div>
<div class="item">40</div>
<div class="item">41</div>
</div>
This solution uses a bottom and top header who are filled in with the selected values and showed/hidden when necessary:
Working Fiddle
Javascript:
function stickItems($parent, itemClass, selectClass) {
// Attach dummy element items
$parent.prepend('<div class="' + itemClass + ' sticky top"></div>');
$parent.append('<div class="' + itemClass + ' sticky bottom"></div>');
var $items = $('.' + itemClass),
$stickyTop = $('.' + itemClass + '.sticky.top'),
$stickyBottom = $('.' + itemClass + '.sticky.bottom');
// Click event registering
$items.click(function (e) {
if (!$(e.target).hasClass('sticky')) {
$items.removeClass(selectClass);
$stickyTop.css('display', 'none');
$stickyBottom.css('display', 'none');
$(this).addClass(selectClass);
}
});
// Scroll event
$parent.scroll(function () {
var $self = $(this);
var $selected = $('.' + itemClass + '.' + selectClass);
var cTop = $selected.offset().top;
var pTop = $self.offset().top;
var cHeight = $selected.height();
var pHeight = $self.height();
if (cTop - pTop <= 0) {
$stickyTop.html($selected.html()).css({
'display': 'block',
'top': $(this).scrollTop()
});
$stickyBottom.css('display', 'none');
} else if (cTop > pTop && cTop < pTop + pHeight) {
$stickyTop.css('display', 'none');
$stickyBottom.css('display', 'none');
} else {
$stickyTop.css('display', 'none');
$stickyBottom.html($selected.html()).css({
'display': 'block',
'bottom': -$(this).scrollTop()
});
}
});
}
stickItems($('.parent'), 'item', 'select');
Css:
body, html {
padding: 0;
margin: 0;
}
body {
padding-top: 200px;
}
.parent {
overflow-x: hidden;
overflow-y: auto;
height: 200px;
position: relative;
}
.item {
padding: 10px 15px;
background-color: tomato;
}
.item.select {
background-color: beige;
}
.item.sticky {
background-color: beige;
display: none;
position: absolute;
left: 0;
right: 0;
z-index: 1;
}
Html:
<div class="parent">
<div class="item sticky top"></div>
<div class="item select">1</div>
<div class="item">2</div>
<!-- ... -->
<div class="item">39</div>
<div class="item">40</div>
<div class="item">41</div>
<div class="item sticky bottom"></div>
</div>

Sticky div on scroll

See this: http://jsfiddle.net/3yx5C/1/
I am trying to make a DIV(the green DIV) from the right column:
1. to be fixed when it meets the HEADER;
2. to be NOT-fixed when you scroll to the top and it meets the other DIVs above(*the grey DIVs);
What I can't achieve is the second part. Any ideas?
I have to mention that the grey DIVs on the right might be more than two, with flexible heights, and they can't be wrapped.
<div id="HEADER"></div>
<div id="WRAPPER">
<div class="layout_right">
<div style="height: 80px; background: gray;"></div>
<div style="height: 80px; background: gray;"></div>
<div id="right_ads">I am sticky!</div>
</div>
<div class="layout_middle">
<div style="width: 300px; height: 200px; background: beige;"></div>
<div style="width: 300px; height: 200px; background: pink;"></div>
<div style="width: 300px; height: 200px; background: blue;"></div>
</div>
</div>
<script type='text/javascript'>
window.addEvent('domready', function() {
function sticky_AD() {
var headerHeight2 = $('HEADER').getSize().y;
var window_top = $(window).getScroll().y + headerHeight2 + 20;
var div_top = $('right_ads').getPosition().y;
if (window_top > div_top){
$('right_ads').addClass('fixed_AD').setStyles({'top': headerHeight2 + 20});
} else {
$('right_ads').removeClass('fixed_AD').setStyles({'top':'auto'});
}
}
$(window).addEvent('scroll', function(){
sticky_AD();
});
sticky_AD();
});
</script>
Check this:
Demo here
I added a new variable var dist = $('right_ads').getPosition().y; to store the original position and use it later and changed your else to a new if : if (window_top < dist) {
Code:
function sticky_AD() {
var headerHeight2 = $('HEADER').getSize().y;
var window_top = $(window).getScroll().y + headerHeight2 + 20;
var div_top = $('right_ads').getPosition().y;
if (window_top > div_top) {
$('right_ads').addClass('fixed_AD').setStyles({
'top': headerHeight2 + 20
});
}
if (window_top < dist) {
$('right_ads').removeClass('fixed_AD').setStyles({
'top': 'auto'
});
}
}
$(window).addEvent('scroll', function () {
sticky_AD();
});
var dist = $('right_ads').getPosition().y;
sticky_AD();

Categories

Resources