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>
Related
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>
I have coded an infinite scroll. When the user scrolls it will load an additional 20 items which makes it a long list.
I want the scroll to loads new items and clear the previous items.
var listElm = document.querySelector('#infinite-list');
// Add 20 items.
var nextItem = 1;
var loadMore = function() {
for (var i = 0; i < 20; i++) {
var item = document.createElement('li');
item.innerText = 'Item ' + nextItem++;
listElm.appendChild(item);
}
}
// Detect when scrolled to bottom.
listElm.addEventListener('scroll', function() {
if (listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight) {
loadMore();
}
});
// Initially load some items.
loadMore();
#infinite-list {
/* We need to limit the height and show a scrollbar */
width: 200px;
height: 300px;
overflow: auto;
/* Optional, only to check that it works with margin/padding */
margin: 30px;
padding: 20px;
border: 10px solid black;
}
/* Optional eye candy below: */
li {
padding: 10px;
list-style-type: none;
}
li:hover {
background: #ccc;
}
<ul id='infinite-list'>
</ul>
If you empty the list before that, would it be ok?
var listElm = document.querySelector('#infinite-list');
// Add 20 items.
var nextItem = 1;
var loadMore = function() {
listElm.innerHTML = ''
for (var i = 0; i < 20; i++) {
var item = document.createElement('li');
item.innerText = 'Item ' + nextItem++;
listElm.appendChild(item);
}
}
// Detect when scrolled to bottom.
listElm.addEventListener('scroll', function() {
if (listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight ) {
loadMore();
}
});
// Initially load some items.
loadMore();
#infinite-list {
/* We need to limit the height and show a scrollbar */
width: 200px;
height: 300px;
overflow: auto;
/* Optional, only to check that it works with margin/padding */
margin: 30px;
padding: 20px;
border: 10px solid black;
}
/* Optional eye candy below: */
li {
padding: 10px;
list-style-type: none;
}
li:hover {
background: #ccc;
}
<ul id='infinite-list'>
</ul>
You can empty the list every time loadMore() is called.
var listElm = document.querySelector('#infinite-list');
// Add 20 items.
var nextItem = 1;
var loadMore = function() {
//Here we empty the list to remove the old results
listElm.innerHTML = ''
//And then load the new items
for (var i = 0; i < 20; i++) {
var item = document.createElement('li');
item.innerText = 'Item ' + nextItem++;
listElm.appendChild(item);
}
}
// Detect when scrolled to bottom.
listElm.addEventListener('scroll', function() {
if (listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight ) {
loadMore();
}
});
// Initially load some items.
loadMore();
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>
I have created a set of dots using div tags inside a div tag. My need is when I drag the last dot, the whole set of dots should move and sit where mouse pointer is placed at present. I tried achieving it using addeventlistner for mouse clicks but failed in my attempt.
Can someone point out the intuition in the segment below?
var dots = document.createElement("div");
dots.className = "dots";
document.body.appendChild(dots);
var dotarray = [];
for (index = 0; index < 10; index++) {
dotarray[index] = document.createElement("div");
dotarray[index].className = "dot";
dots.appendChild(dotarray[index]);
}
dotarray[9].addEventListener("mousedown", function(event) {
if (event.which == 1) {
var currentMousePointerPos, latestMousePointerPos;
currentMousePointerPos = event.pageX;
dotarray[9].addEventListener("mouseup", function(event) {
latestMousePointerPos = event.pageX;
if (currentMousePointerPos != latestMousePointerPos) {
dots.style.marginLeft = currentMousePointerPos + latestMousePointerPos;
}
})
}
})
.dot {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: red;
display: inline-block;
margin-left: 5px;
}
.dots {
border: 1px solid black;
width: 135px;
}
The immediate answer to your question is that dots.style.marginLeft needs to be equal to a string, containing the units.
Hence, this would work:
dots.style.marginLeft = ((currentMousePointerPos+latestMousePointerPos) + "px");
However:
Your mouseup listener only listens to the event that the mouse click is released when it's over the element, so it doesn't do much. If you assign the listener to the whole document, the listener's function would be activated no matter where the mouseup event occurres.
currentMousePointerPos + latestMousePointerPos doesn't represent the final position of the mouse.
If we fix these two issues the will code still operate weirdly, because the left side of the dots element is set to the mouse's last position.
Therefore we just need to subtract the element's width from the marginLeft property.
The following code combines everything I've mentioned:
var dots = document.createElement("div");
dots.className = "dots";
document.body.appendChild(dots);
var dotarray = [];
for (index = 0; index < 10; index++) {
dotarray[index] = document.createElement("div");
dotarray[index].className = "dot";
dots.appendChild(dotarray[index]);
}
dotarray[9].addEventListener("mousedown", function(event) {
if (event.which == 1) {
var currentMousePointerPos;
// Notice how the listener is bound to the whole document
document.addEventListener("mouseup", function(event) {
currentMousePointerPos = event.pageX;
dots.style.marginLeft = ((currentMousePointerPos-dots.offsetWidth) + "px");
})
}
})
.dot {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: red;
display: inline-block;
margin-left: 5px;
}
.dots {
border: 1px solid black;
width: 135px;
}
Is this what you are looking for?
You should use the mousemove event instead to detect any dragging
after mousedown.
var dots = document.createElement("div");
dots.className = "dots";
document.body.appendChild(dots);
var dotarray = [];
for (index = 0; index < 10; index++) {
dotarray[index] = document.createElement("div");
dotarray[index].className = "dot";
dots.appendChild(dotarray[index]);
}
dotarray[9].addEventListener("mousedown", function(event) {
if (event.which == 1) {
window.addEventListener('mousemove', move, true);
/*var currentMousePointerPos, latestMousePointerPos;
currentMousePointerPos = event.pageX;
dotarray[9].addEventListener("mouseup", function(event) {
latestMousePointerPos = event.pageX;
if (currentMousePointerPos != latestMousePointerPos) {
dots.style.marginLeft = currentMousePointerPos + latestMousePointerPos;
}
})*/
}
});
window.addEventListener("mouseup", function(event) {
window.removeEventListener('mousemove', move, true);
});
function move(e){
var div = document.getElementsByClassName('dots')[0];
div.style.position = 'absolute';
div.style.top = -8+e.clientY + 'px';
div.style.left = -135+8+e.clientX + 'px';
}
.dot {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: red;
display: inline-block;
margin-left: 5px;
}
.dots {
border: 1px solid black;
width: 135px;
}
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');
}