I am looking into creating a website which will serve as a a digital leaflet for a musical theatre. The idea is to have an autoscrolling credits list as landingpage. I've looked at examples on codepen to see how this effect is been achieved. But I would also like the user to interact and scroll themselves if they want to. When they stop scrolling the credits will turn back to autoscroll. I didn't find any example who tackles this issue. Does someone of you know a script (JS, or plain css…) that can help me with this?
The most straightforward way is to set up a requestAnimationFrame() function and increment the value accordingly, then set the scroll position to it.
Then add the wheel event to detect when a user scrolls (don't use the 'scroll' event though, it already gets called when you change the scrollTop value of the body), also don't forget to cancel the requestAnimationFrame() function. The code would look something like this:
let body = document.body,
starter = document.querySelector("h1"),
scroll_counter = 0,
scrolled,
auto_scroll_kicked = false;
starter.addEventListener("click", start_scrolling);
function start_scrolling() {
auto_scroll_kicked = true;
body.offsetHeight > scroll_counter
? (scroll_counter += 1.12)
: (scroll_counter = body.offsetHeight);
document.documentElement.scrollTop = scroll_counter;
scroller = window.requestAnimationFrame(start_scrolling);
if (scroll_counter >= body.offsetHeight) {
window.cancelAnimationFrame(scroller);
}
}
window.addEventListener("wheel", (e) => {
if (auto_scroll_kicked) {
window.cancelAnimationFrame(scroller);
scroll_counter = 0;
}
});
Play with the codepen if you'd like:
https://codepen.io/SaltyMedStudent/pen/QWqVwaR?editors=0010
There are many options to use: easing functions and etc, but hope this will suffice for now.
In your auto scroll routine before changing position check if previous position is the same as current scrolling position, if it's not - the user scrolled it:
let el = document.documentElement,
footer = document.getElementById("status").querySelectorAll("td"),
scroll_position = 0,
scroll_speed = 0,
scroll_delta = 1.12,
scroller,
status = "stopped";
el.addEventListener("click", scroll);
info();
function scroll(e)
{
if (e.type == "click")
{
window.cancelAnimationFrame(scroller);
scroll_position = el.scrollTop; //make sure we start from current position
scroll_speed++; //increase speed with each click
info("auto scroll");
}
//if previous position is different, this means user scrolled
if (scroll_position != el.scrollTop)
{
scroll_speed = 0;
info("stopped by user");
return;
}
el.scrollTop += scroll_delta * scroll_speed; //scroll to new position
scroll_position = el.scrollTop; //get the current position
//loop only if we didn't reach the bottom
if (el.scrollHeight - el.scrollTop - el.clientHeight > 0)
{
scroller = window.requestAnimationFrame(scroll); //loop
}
else
{
el.scrollTop = el.scrollHeight; //make sure it's all the way to the bottom
scroll_speed = 0;
info("auto stopped");
}
}
function info(s)
{
if (typeof s === "string")
status = s;
footer[1].textContent = el.scrollTop;
footer[3].textContent = scroll_speed;
footer[5].textContent = status;
}
//generate html demo sections
for(let i = 2, section = document.createElement("section"); i < 6; i++)
{
section = section.cloneNode(false);
section.textContent = "Section " + i;
document.body.appendChild(section);
}
//register scroll listener for displaying info
window.addEventListener("scroll", info);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body
{
font-family: "Roboto", Arial;
user-select: none;
}
section
{
min-height: 100vh;
font-size: 3em;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
section:nth-child(even)
{
background: #0b0d19;
}
section:nth-child(odd)
{
background: #131524;
}
#status
{
position: fixed;
bottom: 0;
color: #fff;
margin: 0.5em;
}
#status td:first-of-type
{
text-align: end;
padding-right: 0.4em;
}
#status td:last-of-type
{
font-weight: bold;
}
<section>
Click to start Scrolling
</section>
<table id="status">
<tr><td>position:</td><td></td></tr>
<tr><td>speed:</td><td></td></tr>
<tr><td>status:</td><td></td></tr>
</table>
Related
I have more than one section thats why i used for loop,
what can i do to add class to my section element?
const sections = document.querySelectorAll('section');
function sectionInViewPort(element) {
return (element.getBoundingClientRect().distance.top >= 0);
}
function AddActiveClass() {
for (s = 0; s <= sections.length; s++) {
//if section is in view port add class "your-active class"
if (sectionInViewPort(sections[s])) {
sections[s].classList.add("your-active-class");
//else remove it
}else {
sections[s].classList.remove("your-active-class");
}
}
}
document.addEventListener('scroll', AddActiveClass);
Few small problems but you're on the right track.
Firstly, try using getBoundingClientRect().top - which should give you a px distance from the top of the window to the item.
Second, you need to be calling your functions for them to work. You need to be calling AddActiveClass whenever the window is scrolled so it does it's checks whenever sections move up or down the page.
Third, you're grabbing all sections on the page but only using the first - i'm assuming this is just test code and you want to be checking all sections (in which case you'd use sections[s] in your code - the sections array using the looping s index.
finally, check your logic - your top >= 0 check is going to add the class to every section below the top of the page - you might want to do something like top > 0 && top < 100 to only hit sections near the top of the page, or add a check so that only one section ever gets the class added.
A rough idea of how your code should look, may need a couple tweaks:
// on window scroll
window.addEventListener('scroll', (e) => {
// get all sections on the page
const sections = document.querySelectorAll('section');
// loop through each section
sections.forEach( section => {
// get px distance from top
const topDistance = section.getBoundingClientRect().top;
// if the distance to the top is between 0-100px
if (topDistance > 0 && topDistance < 100) {
section.classList.add('your-active-class');
// otherwise, remove the class
} else {
section.classList.remove('your-active-class');
}
});
});
You had couple of mistakes in your code. Check the snippet, it should do the magic. And if you have any further questions, just ping me and I try to elaborate my code:
const sections = document.querySelectorAll('section');
const isInViewport = (section) => {
const { top } = section.getBoundingClientRect();
section.classList.toggle('active', top >= 0);
}
const toggleActiveClass = () => {
sections.forEach(isInViewport);
}
document.addEventListener('scroll', toggleActiveClass);
section {
background-color: red;
height: 200px;
}
section.active {
background-color: green;
}
<section>#1</section>
<section>#2</section>
<section>#3</section>
<section>#4</section>
<section>#5</section>
<section>#6</section>
<section>#7</section>
This is my approach to your problem.
Of course you have remove the CSS I used for demonstration purposes, and also you have to remove the code I use inside the AddActiveClass I used for better understanding of what I did. The code to remove is the following:
var span = section.querySelector('span');
span.innerText = distanceFromTop + ' - Greater Than Or Equal to O: ' + (distanceFromTop >= 0);
function AddActiveClass() {
const sections = document.querySelectorAll('section');
sections.forEach(
function(section) {
var distanceFromTop = section.getBoundingClientRect().top;
var span = section.querySelector('span');
span.innerText = distanceFromTop + ' - Greater Than Or Equal to O: ' + (distanceFromTop >= 0);
if( distanceFromTop >= 0) {
section.classList.add("your-active-class");
} else {
section.classList.remove("your-active-class");
}
}
);
}
document.addEventListener('scroll', AddActiveClass);
AddActiveClass();
section {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
section:nth-child(1) {background :#0AF;}
section:nth-child(2) {background :#A0F;}
section:nth-child(3) {background :#AF0;}
section:nth-child(4) {background :#FA0;}
section:nth-child(5) {background :#F0A;}
section:nth-child(6) {background :#0FA;}
section span {
font-size: 32px;
font-weight: bold;
color: #FFF;
text-shadow: 0px 0px 3px #000000,-2px -2px 3px #FF0000,2px 2px 3px #00FF00,-2px 2px 3px #0000FF;
}
section.your-active-class {
background: rgb(2,0,36);
background: linear-gradient(41deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%);
}
<section><span>0</span></section>
<section><span>0</span></section>
<section><span>0</span></section>
<section><span>0</span></section>
<section><span>0</span></section>
<section><span>0</span></section>
I made a multi section landing page with a navbar containing the links to the sections of the page
I need to give an active class to the link of the section closest to the top of the page, I tried using the scroll event listener but it is not working properly
//respond to scrolling by giving navbar active state for current section
``document.addEventListener("scroll", function() {
const scrollPos = document.body.scrollTop;
let pos = 0;
for (let i = 0; i < sec.length; i++) {
// determining pos of current section
pos+=sec[i].offsetHeight ;
let secPos = sec[i].getBoundingClientRect().y;
secPos - scrollPos <= 0 && secPos + pos >= scrollPos ? list[i].classList.add("active") : list[i].classList.remove("active")
}
});```
The scroll event is working properly, but your code does not correctly address the list of elements you are itering.
just try changing list[] to sec[]
document.addEventListener( "DOMContentLoaded", function() {
let sec = document.querySelectorAll('section');
console.log(sec);
sec[0].classList.add("active")
document.addEventListener("scroll", function() {
const scrollPos = document.body.scrollTop;
let pos = 0;
for (let i = 0; i < sec.length; i++) {
// determining pos of current section
pos+=sec[i].offsetHeight ;
let secPos = sec[i].getBoundingClientRect().y;
if ((secPos - scrollPos <= 0) && (secPos + pos >= scrollPos))
sec[i].classList.add("active");
else
sec[i].classList.remove("active");
}
})
})
section {
font-size: 80px;
text-align: center;
width: 300px;
margin: 20px auto;
background-color: pink;
border: 1px solid red;
}
section.active {
border: 1px solid blue;
background-color: cyan;
}
<section>1</section>
<section>2</section>
<section>3</section>
<section>4</section>
<section>5</section>
<section>6</section>
<section>7</section>
<section>8</section>
Using a code snippet I found online https://codepen.io/mattyfours/pen/LNgOWx
I made slight modifications and now, although the scroll/fixed functionality works, my 'fixed' side jumps when scrolling. I added 'background-size: contain' onto the fixed side which only works when scrolling has commenced However, on page load/ when no scrolling has occurred the image remains at its full-size meaning once scrolling begins the image goes from full width to 'contained' and created a jump.
Github:
https://github.com/tavimba/fixed-scroll
The issue can be seen in about.html
javascript:
var window_height;
var header_height;
var doc_height;
var posTop_sticky1;
var posBottom_sticky1;
var posTop_s2;
var posBottom_s2;
$(document).ready(function() {
getValues();
});
$(window).scroll(function(event) {
var scroll = $(window).scrollTop();
if (scroll < posTop_sticky1) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
}
if (scroll > posTop_sticky1) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
$('#sticky1 .sticky').addClass('fixy');
}
if (scroll > posBottom_sticky1) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
$('#sticky1 .sticky').addClass('bottom');
$('.bottom').css({
'max-height': window_height + 'px'
});
}
if (scroll > posTop_s2 && scroll < posBottom_s2) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
$('#s2 .sticky').addClass('fixy');
}
});
function getValues() {
window_height = $(window).height();
doc_height = $(document).height();
header_height = $('header').height();
//get heights first
var height_sticky1 = $('#sticky1').height();
var height_s2 = $('#s2').height();
//get top position second
posTop_sticky1 = header_height;
posTop_s2 = posTop_sticky1 + height_sticky1;
//get bottom position 3rd
posBottom_sticky1 = posTop_s2 - header_height;
posBottom_s2 = doc_height;
}
var rtime;
var timeout = false;
var delta = 200;
$(window).resize(function() {
rtime = new Date();
if (timeout === false) {
timeout = true;
setTimeout(resizeend, delta);
}
});
function resizeend() {
if (new Date() - rtime < delta) {
setTimeout(resizeend, delta);
} else {
timeout = false;
getValues();
}
}
CSS:
section {
width: 100%;
min-height: 100%;
float: left;
position: relative;
}
header {
width: 100%;
height: 5vw;
background-color: black;
float: left;
}
.sticky {
height: 100%;
width: 60%;
float: left;
position: absolute;
}
.sticky.fixy {
position: fixed;
top: 0;
left: 0;
}
.sticky.bottom {
position: absolute;
bottom: 0;
}
.green {
background-image: url(../imgs/front%20view.jpg);
background-size: cover;
}
.stickyBg {
background-image: url(../imgs/bonnets.jpg);
background-size: cover;
}
.scrolling {
float: right;
width: 50%;
padding: 20px;
h5 {
margin-left: 135px;
}
p {
margin-left: 135px;
font-size: 1em;
line-height: 1.5;
}
}
The jump is caused by change of position from absolute to fixed in combination with 100% height.
Besides, the above code has the following flaws:
Max-height assignment looks inconsistent.
JS assumes exactly two sections in HTML: #section1 and #s2. The third section won't work.
Window resize is handled incorrectly. The half-page-scroll logic consists of the two steps: CalculateVars and AdjustDOMElementPositions. For the smooth look these two actions have to be done in 3 cases: onDocumentLoad, onResize and onScroll.
Global vars.
Looks like, it needs some refactoring to get work ;)
<section class="js-half-page-scroll-section"><!-- Get rid of id -->
...
</section>
function halfPageScroll() {
let scrollTop, windowHeight, headerHeight; // and some other common vars
// Calculate vars
scrollTop = $(window).scrollTop();
//...
let repositionSection = function($section) {
let sectionHeight; // and some other vars related to current section
// Some logic
}
$('.js-half-page-scroll-section').each((i, el) => repositionSection($(el)));
}
$(document).ready(halfPageScroll);
$(window).scroll(halfPageScroll);
$(window).resize(halfPageScroll); // TODO: add some debounce wrapper with timeouts
I am trying to make an infinite scroll in my chat. I'm using the scroll event to check if scrolltop < clientHeight and call a function loadMore if it is. This works pretty well as long as you never scroll to the very top. I made a gif to show this (hopefully it makes sense):
If you still have more room to scroll when the older messages get loaded, you keep your place and the scroll bar gets pushed down.
But if you are scrolled all the way to the top when the older messages get loaded, the scroll bar stays pinned to the top and you lose your place (also the scroll event stops being fired, so you stop loading messages unless you scroll down a little)
Has anyone else experienced this? And what did you do to fix it? Any advice appreciated. Thanks!
updated the answer to support 2 directions (up or down) and loading paddings. Please run the snippet in expanded mode, inline preview frame is too small for the scrollable list.
var isLoadingAlready = false;
var upDirection = true; // to load records on top of the list; false to load them to the end of the list
var loadThreshold = 100; // distance to the edge (in pixels) to start loading
var howManyDataLoadsAvailable = 5;
if (upDirection){
$('.scroll')[0].scrollTop = 100000; // scrolling all the way down
$('.scroll').css('paddingTop', loadThreshold);
} else {
$('.scroll').css('paddingBottom', loadThreshold);
}
$('.scroll').on('scroll', function () {
var s = this; // picking DOM element
if (s) { // just to be sure/safe
var scrollableHeight = s.scrollHeight - s.clientHeight;
if (scrollableHeight > 0) {
var scrollTop = s.scrollTop;
var distToTheEdge = upDirection?scrollTop:scrollableHeight - scrollTop;
if (distToTheEdge < loadThreshold && !isLoadingAlready) {
isLoadingAlready = true;
loadMoreRecords(function () { // assuming you have a callback to allow next loading
isLoadingAlready = false;
});
}
}
}
});
loadMoreRecords();
function loadMoreRecords(doneCallback){
$('.scroll').addClass('loading');
// simulating the actual loading process with setTimeout
setTimeout(function(){
// simulated items to insert:
var items = [];
if (howManyDataLoadsAvailable-- > 0){
for (var i = 0; i < 10; i++){
items.push($('<li>').text('msg: '+(i+1)+', parts left: '+howManyDataLoadsAvailable));
}
}
var $se = $('.scroll'); // scrollable DOM element
var $ul = $('.scroll ul');
var se = $se[0];
if (upDirection) {
var hBefore = $ul.height();
$ul.prepend(items);
var hDiff = $ul.height() - hBefore;
se.scrollTop = Math.max(hDiff, loadThreshold);
} else {
$ul.append(items);
se.scrollTop = se.scrollHeight - se.clientHeight - Math.max(se.scrollHeight - se.clientHeight - se.scrollTop, loadThreshold);
}
$se.removeClass('loading');
if (typeof(doneCallback) === 'function'){
doneCallback();
}
}, 500);
}
.scroll{
overflow-y: auto;
max-height: 200px;
border: 2px dashed #aaa;
padding: 0.5em;
margin: 1em;
}
.scroll.loading{
background-color: #f5f5f5;
}
ul{
list-style: none;
padding: 0;
}
li{
padding: 0.5em;
border: 1px solid #eee;
border-radius: 0.5em;
margin: 0.2em;
animation: colorchange 1200ms;
background: white;
box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.05);
}
#keyframes colorchange
{
0% {background: #def;}
100% {background: white;}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="scroll">
<ul></ul>
</div>
PURE JS ONLY PLEASE - NO JQUERY
I have a div with overflow scroll, the window (html/body) never overflows itself.
I have a list of anchor links and want to scroll to a position when they're clicked.
Basically just looking for anchor scrolling from within a div, not window.
window.scrollTo etc. don't work as the window never actually overflows.
Simple test case http://codepen.io/mildrenben/pen/RPyzqm
JADE
nav
a(data-goto="#1") 1
a(data-goto="#2") 2
a(data-goto="#3") 3
a(data-goto="#4") 4
a(data-goto="#5") 5
a(data-goto="#6") 6
main
p(data-id="1") 1
p(data-id="2") 2
p(data-id="3") 3
p(data-id="4") 4
p(data-id="5") 5
p(data-id="6") 6
SCSS
html, body {
width: 100%;
height: 100%;
max-height: 100%;
}
main {
height: 100%;
max-height: 100%;
overflow: scroll;
width: 500px;
}
nav {
background: red;
color: white;
position: fixed;
width: 50%;
left: 50%;
}
a {
color: white;
cursor: pointer;
display: block;
padding: 10px 20px;
&:hover {
background: lighten(red, 20%);
}
}
p {
width: 400px;
height: 400px;
border: solid 2px green;
padding: 30px;
}
JS
var links = document.querySelectorAll('a'),
paras = document.querySelectorAll('p'),
main = document.querySelector('main');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', function(){
var linkID = this.getAttribute('data-goto').slice(1);
for (var j = 0; j < links.length; j++) {
if(linkID === paras[j].getAttribute('data-id')) {
window.scrollTo(0, paras[j].offsetTop);
}
}
})
}
PURE JS ONLY PLEASE - NO JQUERY
What you want is to set the scrollTop property on the <main> element.
var nav = document.querySelector('nav'),
main = document.querySelector('main');
nav.addEventListener('click', function(event){
var linkID,
scrollTarget;
if (event.target.tagName.toUpperCase() === "A") {
linkID = event.target.dataset.goto.slice(1);
scrollTarget = main.querySelector('[data-id="' + linkID + '"]');
main.scrollTop = scrollTarget.offsetTop;
}
});
You'll notice a couple of other things I did different:
I used event delegation so I only had to attach one event to the nav element which will more efficiently handle clicks on any of the links.
Likewise, instead of looping through all the p elements, I selected the one I wanted using an attribute selector
This is not only more efficient and scalable, it also produces shorter, easier to maintain code.
This code will just jump to the element, for an animated scroll, you would need to write a function that incrementally updates scrollTop after small delays using setTimeout.
var nav = document.querySelector('nav'),
main = document.querySelector('main'),
scrollElementTo = (function () {
var timerId;
return function (scrollWithin, scrollTo, pixelsPerSecond) {
scrollWithin.scrollTop = scrollWithin.scrollTop || 0;
var pixelsPerTick = pixelsPerSecond / 100,
destY = scrollTo.offsetTop,
direction = scrollWithin.scrollTop < destY ? 1 : -1,
doTick = function () {
var distLeft = Math.abs(scrollWithin.scrollTop - destY),
moveBy = Math.min(pixelsPerTick, distLeft);
scrollWithin.scrollTop += moveBy * direction;
if (distLeft > 0) {
timerId = setTimeout(doTick, 10);
}
};
clearTimeout(timerId);
doTick();
};
}());
nav.addEventListener('click', function(event) {
var linkID,
scrollTarget;
if (event.target.tagName.toUpperCase() === "A") {
linkID = event.target.dataset.goto.slice(1);
scrollTarget = main.querySelector('[data-id="' + linkID + '"]');
scrollElementTo(main, scrollTarget, 500);
}
});
Another problem you might have with the event delegation is that if the a elements contain child elements and a child element is clicked on, it will be the target of the event instead of the a tag itself. You can work around that with something like the getParentAnchor function I wrote here.
I hope I understand the problem correctly now: You have markup that you can't change (as it's generated by some means you have no control over) and want to use JS to add functionality to the generated menu items.
My suggestion would be to add id and href attributes to the targets and menu items respectively, like so:
var links = document.querySelectorAll('a'),
paras = document.querySelectorAll('p');
for (var i = 0; i < links.length; i++) {
links[i].href=links[i].getAttribute('data-goto');
}
for (var i = 0; i < paras.length; i++) {
paras[i].id=paras[i].getAttribute('data-id');
}