Simple Scrollspy without Bootstrap on custom Nav Component - javascript

I have a Table of Contents with id #TableOfContents in which each href points to a h2 or h3.
The problem I am having is that once the Heading, h2 or h3 is observed by intersection observer, Entry for that is highlighted by adding class side-active for that link in #TableOfContents, but as soon as the long content (such as p paragraph) after the heading comes in viewport the highlight for that section is removed since the heading is not in viewport.
This is a problem since I want the section (h2, h3) to still be highlighted until next h2 or h3 doesn't cross half of viewport.
What can I do?
window.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.getAttribute('id');
if (entry.intersectionRatio > 0) {
document.querySelector(`#TableOfContents a[href="#${id}"]`).classList.add('side-active');
} else {
document.querySelector(`#TableOfContents a[href="#${id}"]`).classList.remove('side-active');
}
});
});
toc = document.querySelectorAll('#TableOfContents a');
// get content so that link refer to it
toc.forEach(function (link) {
var id = link.getAttribute("href");
var element = document.querySelector(id);
observer.observe(element);
});
});
Text is highlighted when heading in viewport
Text is not highlighted once heading is not in viewport

So the thing was we needed to calculate containerBottom by:
adding current container's top + next container's top
Or if it is last container then,
current container's top + it's outerHeight()
Once that; if ith container is in scrollposition add class to it And remove class from all containers previous to ith container (line no 18-21) (reverse loop)
$(function () {
// Table of contents (`ul`) that contains `a` tag in `li` which we want to highlight
var sectionIds = $('#TableOfContents a');
$(document).on('scroll', function(){
sectionIds.each(function(i, e){
var container = $(this).attr('href');
var containerOffset = $(container).offset().top; // container's top
var nextContainer = $(sectionIds[i+1]).attr('href')
if (i != sectionIds.length-1) {
// if this container isn't last container
var containerHeight = $(nextContainer).offset().top;
} else {
// last container's height will be outerHeight
var containerHeight = $(container).outerHeight();
}
var containerBottom = containerOffset + containerHeight;
var scrollPosition = $(document).scrollTop();
if(scrollPosition < containerBottom - 20 && scrollPosition >= containerOffset - 20){
for (var j = i; j >= 0; j--) {
$(sectionIds[j]).removeClass('active');
}
$(sectionIds[i]).addClass('active');
} else{
$(sectionIds[i]).removeClass('active');
}
});
});
});
Check the first heading ("Line Equations" h1) that is highlighted stays highlighted even though the title is not in viewport until next sub heading ("Distance of a point from a line") comes into view.

Related

Scrolling upwards

I want to make a function that will automatically display items as the user scrolls upwards. Think messages, where older ones are up top, but we start at the bottom with the newest ones. I have another function that loads items at the bottom of a container, and as it does so, the current items remain in position, and scrollbar is smaller. We are not scrolled to the bottom. However, with the other direction, when I prepend items to an array, it scrolls all the way to the top, displaying the top of the loaded items, instead of remaining in the same place and letting the user scroll up as needed.
My code for the bottom scroll is this:
attachScrollWatcher: function (element, offset, callback) {
console.log('scrolling');
var contentHeight = element.scrollHeight;
var yOffset = element.scrollTop;
var y = yOffset + element.offsetHeight;
if (y >= ( contentHeight - offset ))
{
callback();
}
}
This function is attached to an object's onscroll event. However, now I need to make a function that does the opposite, going upwards. Any ideas how this can be implemented?
Basically, when scrollTop === 0 then you're at the top and you need to load a new item..
attachScrollWatcher: function (element, offset, callback) {
if(!element.scrollHeight) callback();
}
The problem is, loading a new item will keep the scrollTop at zero, so the user would have to scroll down and then scroll back up in order for the callback to be triggered again. So, what you wanna do is calculate the scrollHeight before the new item is added and then again after the item is added and then manually set the scrollTop to the difference between the original and the new scrollHeight.
Check out my example attachScrollListener method below...
class upScroller{
constructor(ele = document.body){
this.renderedItems = 0;
this.ele = ele; var i=0;
this.initBoxContents();
}
initBoxContents(){
if(this.ele.scrollHeight == this.ele.clientHeight)
this.populateNextItem().then(()=>this.initBoxContents());
else{
this.ele.scrollTop = this.ele.clientHeight;
this.attachScrollListener();
}
}
getNextItem(){
// Do something here to get the next item to render
// preferably using ajax but I'm using setTimeout
// to emulate the ajax call.
return new Promise(done=>setTimeout(()=>{
this.renderedItems++;
done(`<p>This is paragraph #${this.renderedItems}</p>`);
},50));
}
populateNextItem(){
return new Promise(done=>{
this.getNextItem().then(item=>{
this.ele.insertAdjacentHTML('afterbegin', item);
done();
});
});
}
attachScrollListener(){
this.ele.addEventListener('scroll', ()=>{
if(this.ele.scrollTop) return;
var sh = this.ele.scrollHeight;
this.populateNextItem().then(()=>{
this.ele.scrollTop = this.ele.scrollHeight - sh;
});
});
}
}
var poop = document.getElementById('poop');
new upScroller(poop);
#poop{ height: 300px; overflow: auto; border:1px solid black;}
<div id=poop></div>
I've posted this here as well....
Something like this may work.
attachScrollWatcher: function (element, offset, callback) {
console.log('scrolling');
var contentHeight = element.scrollHeight;
var yOffset = element.scrollTop;
var y = yOffset + element.offsetHeight;
if (y >= ( contentHeight - offset ))
{
callback();
} else {
callbackGoingUp();
}
}

Add class when element is at top of window

I'm trying to create sticky headers that when you scroll to a div the head state becomes fixed and stays in view, when the div has come to an end and scrolls out of view I want the title to then become absolute and stay at the bottom of its parent.
I've got the initial part working only I'm struggling on adding the 'absolute' class...
https://jsfiddle.net/yw313vf2/1/
function fixTitle() {
$('.service-pane').each(function() {
var $this = $(this);
var offset = $this.offset().top;
var scrollTop = $(window).scrollTop();
if (scrollTop > offset) {
$this.addClass('fixed');
} else {
$this.removeClass('fixed');
}
});
}
$(window).scroll(fixTitle);
So I had to run another check within the function to see if when scrolled the end of my div had reached the top of the window and if so add an additional class...
function fixTitle() {
$('.service-pane').each(function() {
var $this = $(this);
var offset = $this.offset().top - 50;
var scrollTop = $(window).scrollTop();
if (scrollTop > offset) {
$this.addClass('fixed');
if ($this[0].getBoundingClientRect().bottom < $('.manifesto').height() + 50) {
$this.addClass('absolute');
} else {
$this.removeClass('absolute');
}
} else {
$this.removeClass('fixed');
}
});
}

use jquery to add class both on click of nav item AND scroll past div

I am looking to use jquery to add a class of "selected" both on clicking of a nav item as well as scrolling past that section div.
So far I have got the class added on clicking of the nav item but I am unsure how to neatly add the same affect when scrolling past each section div.
Here's my script for adding the class on click (which is pretty straightforward and standard):
$('#fixednav li').click(function(){
$('#fixednav li').removeClass("selected");
$(this).addClass("selected");
});
...It all works great when user clicks the items in the fixed menu. But how do I add the selected class when user just scrolls down the page without using the fixed menu? Each section has a div id so I would think it would be easy to add something in to add the class on scroll past the div id....
Here's my fiddle
Scroll down a bit to see the fixed nav pop in....
i had the same issue and i used scrollspy to fix it here is the link and demo on bootstrap :
scrollspy
if you get some pb please let me know.
Check if this helps you fiddle
This is the js used, check the fiddle for html and css.
var hei = $("#header").height();
$(window).scroll(function(){
if($(window).scrollTop() > hei){
$("#header").addClass('fixed');
$('.body-wrapper').css('margin-top',hei);
}else{
$("#header").removeClass('fixed');
$('.body-wrapper').css('margin-top','0');
}
});
$("#header li a").on('click', function(event){
event.preventDefault();
var header_height = $("#header").height();
var target = $(this).attr('href');
$('html, body').animate({
scrollTop: $(target).offset().top-header_height
}, 1000);
});
var aChildren = $("#header li").children(); // find the a children of the list items
var aArray = []; // create the empty aArray
for (var i=0; i < aChildren.length; i++) {
var aChild = aChildren[i];
var ahref = $(aChild).attr('href');
aArray.push(ahref);
} // this for loop fills the aArray with attribute href values
$(window).scroll(function(){
var windowPos = $(window).scrollTop(); // get the offset of the window from the top of page
var windowHeight = $(window).height(); // get the height of the window
var docHeight = $(document).height();
for (var i=0; i < aArray.length; i++) {
var theID = aArray[i];
var divPos = $(theID).offset().top-hei-30; // get the offset of the div from the top of page
var divHeight = $(theID).height(); // get the height of the div in question
if (windowPos >= divPos && windowPos < (divPos + divHeight)) {
$("a[href='" + theID + "']").addClass("nav-active");
} else {
$("a[href='" + theID + "']").removeClass("nav-active");
}
}
});

Ordering a graphical list via mouse dragging using JavaScript

NOTE: Exact description of question follows CSS below. Sample code can be seen in this fiddle.
I have a parent div with a list of child divs within it, that looks like the following:
HTML for said container and children is:
<div class="categories_container">
<div class="category one">One</div>
<div class="category two">Two</div>
<div class="category three">Three</div>
<div class="category four">Four</div>
<div class="category five">Five</div>
<div class="category six">Six</div>
</div>
Where the classes .one, .two, .three, etc... are their relative position in the list.
The children elements are positioned with absolute positioning, within their parent.
CSS as follows (some properties not shown for simplicity):
.categories_container {
height: 324px;
width: 100%;
position: relative;
}
.category {
height: 50px;
width: 98%;
position: absolute;
left: 0px;
z-index: 0;
}
.one {
top: 0px;
}
.two {
top: 54px;
}
.three {
top: 108px;
}
.four {
top: 162px;
}
.five {
top: 216px;
}
.six {
top: 270px;
}
As can be seen in this fiddle, you can click (and hold) on any one of the child elements and move it up and down within the parent div. When you release the mouse, the selected child snaps back to its original position.
Question:
How can I detect if the selected element has been dragged overtop of another? I don't only want to know if they are overlapping, but would like to put a range on it. Something like...
if(center of current child is overtop a set range within another child){
do stuff...
}
What I'd like to do for now (as a proof of concept) is to have the underneath child's background color change WHILE the vertical center of the selected child is within the range 0.4-0.6 of the bottom child's height. If the selected child is dragged out of said region, the background should change back.
I've tried something like:
$('.category').mouseover(function(){
if(dragging){
... execute code...
}
});
But it seems that if I am dragging one element over the other, the bottom element cannot see the mouse, and so the function is never executed.
Also:
I've tried a few different methods to keep the cursor as a pointer while dragging, but no matter what it switches to the text cursor whilst dragging. So any help with that would also be appreciated.
For the pointer thing I've tried putting $(this).css('cursor', 'pointer'); in the mousedown and mouse move functions, but to no avail.
Thanks in advance! Sorry if any of this is confusing.
Here is the solution I came up with, purely with JS and JQuery, with no external libraries required and without using JQueryUI Sortables.
HTML:
<div class="list_container">
<div class="list_item">One</div>
<div class="list_item">Two</div>
<div class="list_item">Three</div>
<div class="list_item">Four</div>
<div class="list_item">Five</div>
<div class="list_item">Six</div>
</div>
where list_container holds the individual list_item elements. Is it the latter of the two which can be moved around to create your sorted list. You can put just about anything you'd like within list_item and it'll still work just fine.
CSS:
.list_container {
position: relative;
}
.list_item {
position: absolute;
z-index: 0;
left: 0px;
}
.list_item.selected {
z-index: 1000;
}
Please visit this fiddle for the full list of CSS rules (only necessary ones are shown above).
JavaScript:
I'll go through this bit-by-bit and then show the full code at the bottom.
First off, I defined an array that matches up index numbers with their written counterparts
var classes = new Array("one", "two", "three", ...);
This is used to create classes dynamically (upon page load). These classes are used to order the list. You are only required to populate this array with as many items as you will have in your list. This is the one downfall of the code I have written and am unsure of how to overcome this issue (would be VERY tedious to enter in the elements for a list of hundreds of items, or more!)
Next, a few other variables:
var margin = 2; // Space desired between each list item
var $el; // Used to hold the ID of the element that has been selected
var oldPos = 0; // The position of the selected element BEFORE animation
var newPos = 0; // The position of the selected element AFTER animation (also current position)
var dragging = false; // Whether or not an item is being moved
var numElements = $('.list_container > div').length;
// selectionHeight is the height of each list element (assuming all the same height)
// It includes the div height, the height of top and bottom borders, and the desired margin
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = ''; // classInfo will be populated with the information that is used to dynamically create classes upon page load
When page loads, go through each list_item and assign it a class according to its initial position in the list. Also add to classInfo the location of the TOP of the list item.
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
Now, using classInfo that was created above, dynamically write the classes to the page.
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
The above bit of code will write the required classes into the HTML of the page. If you view the source of the page, you can see the classes in the head of the page.
Now for the ordering part. First, mousedown
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY; // Gets the current mouse position
startT = parseInt($el.css('top')); // Gets the current position of the TOP of the item
$el.addClass('selected'); // Adding class brings it to top (z-index) and changes color of list item
});
Next, the mousemove and mouseup functions are tied together
$(window).mousemove(function (ev) { // Use $(window) so mouse can leave parent div and still work
if (dragging) {
$el.attr('class', 'list_item') // Remove the numbered class (.one, .two, etc)
$el.addClass('selected'); // Add this class back for aesthetics
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight); // Determine what the current position of the selected list item is
// If the position of the list item has changed, move the position's current element out of the way and reassign oldPos to newPos
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false; // User is no longer dragging
$el.removeClass('selected'); // Element is no longer selected
setNewClass($el, newPos); // Set the new class of the moved list item
$el.css('top', (newPos - 1) * selectionHeight); // Position the moved element where it belongs. Otherwise it'll come to rest where you release it, not in its correct position.
});
Finally, the three functions getPos, moveThings and setNewClass are as follows:
function getPos(a, b) { // a == newTop, b == selectionHeight
return Math.round( (a/b) + 1 );
}
getPos works by finding out which region the selected element is currently in. If newTop is less than .5b, then it is in region 1. If between .5b and 1.5b, then it is region 2. If between 1.5b and 2.5b, then in region 3. And so on. Write out a few cases on a piece of paper and it'll make sense what is happening.
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1]; // What is the current class of the item that will be moved
var $newEl = $('.list_container .' + first); // ID of element that will be moved
if (a < b) { // oldPos less than newPos
var second = classes[b - 2]; // The new class of the moved element will be one less
var newTop = parseInt($newEl.css('top')) - c; // Top of element will move up
} else { // oldPos more than newPos
var second = classes[b]; // The new class of the moved element will be one more
var newTop = parseInt($newEl.css('top')) + c; // Top of element will move down
}
// The following line of code is required, otherwise the following animation
// will animate of from top=0px to the new position (opposed to from top=currentPosition)
// Try taking it out and seeing
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first); // Remove the current numbered class of element to move
// Move element and remove the added style tags (or future animations will get buggy)
$newEl.animate({top: newTop}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second); // Add the new numbered class
return false; // Cleans up animations
}
The function above is what does the actual animation part and moves the list items around to accommodate the selected list item.
function setNewClass(e, a) { // e == selected element, a == newPos
// Remove 'selected' class, then add back the 'list_item' class and the new numbered class
e.attr('class', 'list_item').addClass(classes[a-1]);
}
** All JavaScript together: **
var classes = new Array("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeem", "eighteen", "nineteen", "twenty", "twentyone", "twentytwo", "twentythree", "twentyfour");
$(document).ready(function () {
var margin = 2;
var $el;
var oldPos = 0;
var newPos = 0;
var dragging = false;
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = '';
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY;
startT = parseInt($el.css('top'));
$el.addClass('selected');
});
$(window).mousemove(function (ev) {
if (dragging) {
$el.attr('class', 'list_item')
$el.addClass('selected');
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight);
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false;
$el.removeClass('selected');
setNewClass($el, newPos);
$el.css('top', (newPos - 1) * selectionHeight);
});
});
function getPos(a, b) { // a == topPos, b == selectionHeight
return Math.round((a / b) + 1);
}
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1];
var $newEl = $('.list_container .' + first);
if (a < b) { // oldPos less than newPos
var second = classes[b - 2];
var newTop = parseInt($newEl.css('top')) - c;
} else { // oldPos more than newPos
var second = classes[b];
var newTop = parseInt($newEl.css('top')) + c;
}
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first);
$newEl.animate({
top: newTop
}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second);
return false; // Cleans up animations
}
function setNewClass(e, a) { // e == selected element, a == newPos
e.attr('class', 'list_item').addClass(classes[a - 1]);
}

Sticky Sidebar that only sticks when sidebar bottom is at window bottom

I have a 2 column layout. The left column is way longer than the sidebar. I want the sidebar only to stick when its bottom reaches the bottom of the browser window. So the user can continue to scroll down the left column content while the right sidebar sticks. I've seen a lot of sticky questions here, however this particular situation still stumps me. I also have a sticking headline bar on the left column that i've successfully gotten to stick.
Here's a demo of what I've done in jsfiddle!
and here's a quick look at the js I am trying out.
$(function(){
var headlineBarPos = $('.headlineBar').offset().top; // returns number
var sidebarHeight = $('.sticky-sidebar-wrap').height();
var sidebarTop = $('.sticky-sidebar-wrap').offset().top;
var windowHeight = $(window).height();
var totalHeight = sidebarHeight + sidebarTop;
$(window).scroll(function(){ // scroll event
var windowTop = $(window).scrollTop(); // returns number
// fixing the headline bar
if (headlineBarPos < windowTop) {
$('.headlineBar').addClass('sticky').css('top', '0');
}
else {
$('.headlineBar').removeClass('sticky').removeAttr('style');
}
if(sidebarHeight < windowTop) {
$('.sticky-sidebar-wrap').addClass('sticky').css('top', '0');
} else {
$('.sticky-sidebar-wrap').removeClass('sticky').removeAttr('style');
}
console.log(windowTop);
});
console.log(headlineBarPos);
console.log(sidebarHeight);
console.log(sidebarTop);
});
I hope I got it right, when the bottom of the sidebar comes into the view, then stick?
I created another div at the bottom of the sidebar (inside the sidebar).
When that comes into view, it sticks.
http://jsfiddle.net/Z9RJW/10/
<div class="moduleController"></div> //inside the sidebar
and in js
$(function () {
var headlineBarPos = $('.headlineBar').offset().top; // returns number
var moduleControllerPos = $('.moduleController').offset().top; // returns number
var sidebarHeight = $('.sticky-sidebar-wrap').height();
var sidebarTop = $('.sticky-sidebar-wrap').offset().top;
var windowHeight = $(window).height();
var totalHeight = sidebarHeight + sidebarTop;
$(window).scroll(function () { // scroll event
var windowTop = $(window).scrollTop(); // returns number
// fixing the headline bar
if (headlineBarPos < windowTop) {
$('.headlineBar').addClass('sticky').css('top', '0');
} else {
$('.headlineBar').removeClass('sticky').removeAttr('style');
}
if (moduleControllerPos < windowTop + windowHeight) {
$('.sticky-sidebar-wrap').addClass('sticky').css('top', '0');
} else {
$('.sticky-sidebar-wrap').removeClass('sticky').removeAttr('style'); }
console.log(windowTop);
});
console.log(headlineBarPos);
console.log(sidebarHeight);
console.log(sidebarTop);
});
I hope it helps.
Something like:
if (sidebar.top + sidebar.height < window.scrolltop + window.height) {
// set sticky
}
and set sticky needs to take into account that the sidebar may be higher than the viewport, so:
sidebar.top = sidebar.height - window.height // this will be a negative number

Categories

Resources