How to highlight the current section the user is viewing in javascript? - javascript

I am coding a simple navigation bar for a project that has four sections, and I made it interactive enough to have a specific color when hovering/clicking on a section and then it returns back to its original color after clicking.
But what if I want the selected section to still be colored/highlighted when a user is viewing it?
So if the hovering color is coded blue, i want the section in the Navbar to still be blue when a user has selected it, and then changes when a user selects another section. Here's my code so far.
// The mouse hover functiona and commands. Here we specificy the color of the buttons/mouse
// when the user clicks on them, there's a color for hovering/clicking
// and a color for leaving the button
function mouseOver () {
let anchor = document.getElementsByTagName('a');
for (i = 0; i < anchor.length; i++) {
anchor[i].addEventListener('mouseover', function handleMouseOver() {
event.target.style.backgroundColor = "#72a6ca";
event.target.style.color = "#fff";
})
//the color returns to its normal state after clicking away
anchor[i].addEventListener('mouseout', function handleMouseOut() {
event.target.style.backgroundColor = "rgb(220, 220, 220)";
event.target.style.color = "black";
})
}
}
and here is my navbar display code
function navBarStyle () {
let anchor = document.getElementsByTagName('a');
let styles = `
display: flex;
flex-direction: row;
align-items: stretch;
color: #000;
text-decoration: none;
margin: 0 0.5em 0 0.5em;
padding: 0.5em;
background-color: rgb(220, 220, 220);
font-size: large;
transform:translateX(-0.5em);
`;
for (i = 0; i < anchor.length; i++) {
anchor[i].setAttribute('style', styles);
} }
if i was vague enough i am sorry, but any help would be appreciated to put me on the right track

Firstly, a note for your current implementation. It works and it is pretty well coded. But for this thing browsers offer native functionality using the :hover selector and it would be better to use than to reinvent it.
I don't have your HTMl but you would most likely need to add a class to each 'a' tag in the nav, something like this:
<nav>
Link 1
Link 2
</nav>
and then you would need a style tag in the head (or better, external css)
<head>
...
<style>
.nav-link {
background-color: 72a6ca;
color: #fff;
}
.nav-link:hover {
background-color: rgb(220, 220, 220);
color: black;
}
</style>
</head>
As for the current section, your best bet would be to use https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
See here for an example: Intersection observer API scroll aware navigation
or this codepen: https://codepen.io/mishunov/pen/opeRdL
Using IntersectionObserver you can detect when the user scrolls in/out of the section. You can toggle another class on and off of the related nav-link then. For example - say you toggle the .current class, your style could look like this to style both cases (hovering and currently scrolled) in 1 place:
.nav-link:hover,
.nav-link.current {
background-color: rgb(220, 220, 220);
color: black;
}

You can make a class named active like this
.active {
backgroundColor: #72a6ca;
color: #fff;
}
and assign it to each anchor that's clicked(or hovered), simultaneously remove .active from the other anchors
let anchors = document.getElementsByTagName('a');
for (let anchor of anchors) {
anchor.addEventListener('mouseover', function handleMouseOver() {
const target = event.currentTarget;
if (target.classList.contains('active')) {
target.classList.remove('active')
} else {
[...anchors].forEach((anchor) => anchor.classList.remove('active'))
target.classList.add('active')
}
})
}
If you want to give the class active to the anchors in viewPort use this code:
const anchors = document.getElementsByTagName('a');
const isInViewport = el => {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
const run = () =>
anchors.forEach(item => {
if (isInViewport(item)) {
item.classList.add('active');
}
});
// Events
window.addEventListener('load', run);
window.addEventListener('resize', run);
window.addEventListener('scroll', run);

Related

Store Animation State When Coming Back To URL

I have a simple JS scroll event that when an element gets to within 50px of the top of the window the header animates and changes colour, which is done by using getBoundingClientRect().top < 50 on a trigger element. This functionality is only on the home page of the site.
Is there anyway of having it so when a user visits another URL/page on the site, and then comes back to this page via the browsers back arrow, that the previous animation state is still applied? If the page reloads and starts at the top again it doesn't matter, but if you click back to the page that uses this code, the menu transition happens even if you return to part of the page that was past the trigger point. I don't want to force the page to the top each time because this page is going to have downloadable and searchable info on, so that it would be real pain to be sent back to the top of that page each time.
I've given a working example below and via the CodePen link, the problem is of course on CodePen and StackOverflow when you go to a different URL and then click back to URL in question it actually reloads the page from scratch again, which doesn't happen as standard browser behaviour on day-to-day websites.
Codepen: https://codepen.io/anna_paul/pen/bGvPWRj
In that back end I'm using PHP, and I do have access to this is there needs to be a server side solution.
Any ideas or suggestions appreciated.
Note: On the actual site this scroll event is invoked via a debounce function, but I have removed this for code simplicity.
let triggerElement = document.getElementById('trigger-element'),
header = document.getElementById('h')
let menuChange = function() {
if(triggerElement.getBoundingClientRect().top < 50) {
header.style.background = 'black'
header.style.transition = '1s'
} else {
header.style.background = 'red'
header.style.transition = '.15s'
}
}
window.addEventListener('scroll', menuChange)
body {
margin: 0;
height: 200vh;
}
#h {
display: flex;
justify-content: center;
background: red;
color: #fff;
position: fixed;
top: 0;
width: 100%;
}
#trigger-element {
margin-top: 150px;
padding: 1rem;
background:blue;
color: #fff;
}
<header id="h">
<p>HEADER CONTENT</p>
</header>
<div id="trigger-element">Trigger Element</div>
I recommend using localStorage for this particular use case, because it can easily be implemented alongside your current method:
const triggerElement = document.getElementById('trigger-element');
const header = document.getElementById('h');
const animationTriggered = localStorage.getItem('animationTriggered') === 'true';
let initialLoad = true;
const menuChange = function() {
if (animationTriggered && initialLoad) {
header.style.background = 'black';
} else if (triggerElement.getBoundingClientRect().top < 50) {
header.style.background = 'black';
header.style.transition = '1s';
localStorage.setItem('animationTriggered', 'true');
} else {
header.style.background = 'red';
header.style.transition = '.15s';
localStorage.setItem('animationTriggered', 'false');
}
initialLoad = false;
}
window.addEventListener('scroll', menuChange);
This will remember the previous state and apply the black background color if the animation was previously triggered. This adds a small amount of overhead, but in a real-world scenario it should not have any noticeable impact on the performance of the application.

how do i trigger onMouseEnter for elements behind other elements

I'm trying to trigger mouseEnter event when mouse is on top of multiple elements.
I want both mouseEnter events to trigger when the mouse is at the center, and preferably for both to turn yellow.
Run the code snippet below for an example:
<!DOCTYPE html>
<html>
<head>
<style>
div:hover {
background-color: yellow;
}
div {
width: 100px;
height:100px;
background:green;
border: 2px solid black;
}
.second {
transform:translateX(50%) translateY(-50%);
}
</style>
<script>
function onhover(){console.log('hovered')}
</script>
</head>
<body>
<div onmouseenter=onhover()></div>
<div onmouseenter=onhover() class='second'></div>
</body>
</html>
According to MDN, the mouseenter event does not bubble, whereas the mouseover event does. However, even if it DID bubble, your elements currently have no relation to one another, thus the mouse events are captured by the upper element.
One possible way around this is with the amazing elementsFromPoint function in JavaScript, which makes quick work of solving your issue:
// Only the IDs of the elments you are interested in
const elems = ["1", "2"];
// Modified from https://stackoverflow.com/a/71268477/6456163
window.onload = function() {
this.addEventListener("mousemove", checkMousePosition);
};
function checkMousePosition(e) {
// All the elements the mouse is currently overlapping with
const _overlapped = document.elementsFromPoint(e.pageX, e.pageY);
// Check to see if any element id matches an id in elems
const _included = _overlapped.filter((el) => elems.includes(el.id));
const ids = _included.map((el) => el.id);
for (const index in elems) {
const id = elems[index];
const elem = document.getElementById(id);
if (ids.includes(id)) {
elem.style.background = "yellow";
} else {
elem.style.background = "green";
}
}
}
div {
width: 100px;
height: 100px;
background: green;
border: 2px solid black;
}
.second {
transform: translateX(50%) translateY(-50%);
}
<div id="1"></div>
<div id="2" class="second"></div>
I think that you can not without javascript, and with it it's a bit tricky, you have to check on every mousemove if the coordinates of the mouse are in de bounding box of the element, this fill fail with elements with border radius but for the others it's ok
<script>
var hovered=[]
function addHover(element){hovered.push(element)}
function onhover(element){console.log("hovered",element)}
function onCustomHover(e){
hovered.forEach((el,i)=>{
let bounds=el.getBoundingClientRect()
if (e.pageX > bounds.left && e.pageX < bounds.bottom &&
e.pageY > bounds.top && e.pageY < bounds.right ) {
onhover(i);
}
})
}
</script>
</head>
<body>
<div id="div1"></div>
<div id="div2" class='second'></div>
<script>
document.body.addEventListener('mousemove', onCustomHover, true);//{capture :false});
addHover(document.getElementById("div1"))
addHover(document.getElementById("div2"));
</script>
I would appreciate if you could rate the answer if that was usefull to you because I can not make comments yet <3
It will be easier to change your code a little bit.
ex. Add to your div elements class box.
Add to your styles class with name hovered which will look like:
.hovered {
background-color: yellow;
}
Into JS(between script tag) add event listeners (code not tested, but idea is shown), also move script to place before closing body tag:
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
box.addEventListener('mouseover', () => {
boxes.forEach(b => b.classList.add('hovered'));
});
box.addEventListener('mouseout', () => {
boxes.forEach(b => b.classList.remove('hovered'));
});
});
The problem is that elements are blocking the mouse such that elements in the background do not receive the event. With the exception that events bubble to the parent.
Given that you could change your markup slightly to get this effect.
First add a class to your boxes so we can easily find them in JavaScript:
<div class="box"></div>
<div class="box second"></div>
Then adapt the CSS such that this background change is toggled with a class instead:
.box.hovered {
background-color: yellow;
}
And then the JavaScript:
// Get all box elements
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
// For each box attach a listener to when the mouse moves
box.addEventListener('mousemove', (ev) => {
// Get the position of the mouse
const { x, y } = ev;
boxes.forEach(b => {
// for each box get it's dimension and location
const rect = b.getBoundingClientRect();
// check if the pointed is in the box
const flag = x > rect.left && x < rect.right && y > rect.top && y < rect.bottom;
// toggle the class
b.classList.toggle('hovered', flag);
});
});
});
This can be improved a lot, especially if you have more boxes by getting the rectangles beforehand and then using the index in the forEach to link the box to it's rectangle:
const boxes = document.querySelectorAll('.box');
const rects = [...boxes].map(box => box.getBoundingClientRect());
Another improvement is to use the fact that events bubble to the parent, that means you could wrap all boxes in one parent and only add a listener to this parent.

css class gets permanently added to button

I have this piece of code for drumkit project for playing audio and add an transition effect to the pressed button. Try here drumkitProject
CSS
.key {
border: .4rem solid black;
border-radius: 10%;
margin:1rem;
font-size: 1.5rem;
padding: 1rem .5rem;
transition: all .07s;
width: 10rem;
text-align: center;
color: white;
background: rgba(0,0,0,0.4);
text-shadow: 0 0 .5rem black;
}
.playing {
transform: scale(1.1);
border-color: #ffc600;
box-shadow: 0 0 1rem #ffc600;
}
Javascript
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const pressedKey = document.querySelector(
`.key[data-key="${e.keyCode}"]`
);
if (!audio) return;
pressedKey.classList.add("playing");
audio.currentTime = 0;
audio.play();
}
function stopSound(e) {
if (e.propertyName !== "transform") return;
this.classList.remove("playing");
}
window.addEventListener("keydown", playSound);
const keys = document.querySelectorAll(".key");
for (let index = 0; index < keys.length; index++) {
keys[index].addEventListener("transitionend", stopSound);
}
When I keep the button pressed the transition effect gets permanently added to the button and the button does not return back to normal. Why is that happening when I have removed the class as soon as the transition gets over.
Code:https://github.com/heysujal/drumkit2
I found out that the effect is permanently added when the transitionend event is blocked from firing (like I said in the comments).
To fix this issue, you can just add a setTimeout in playSound() function, to remove the class, after certain duration.
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const pressedKey = document.querySelector(
`.key[data-key="${e.keyCode}"]`
);
if (!audio) return;
pressedKey.classList.add("playing");
audio.currentTime = 0;
audio.play();
setTimeout(() => pressedKey.classList.remove("playing"), 150);
}
Try it out on fiddle.
It seems to be a bug in the way how transitions are being handled for expensive properties like transition or box-shadow. I managed to reproduce this behavior in Chrome, Firefox and Safari, but looks like Chrome always does it, while Firefox and Safari are more random.
An easy fix for you would be to replace transform in if (e.propertyName !== "transform") return; with a cheaper property, like border-top-color:
if (e.propertyName !== "border-top-color") return;
But I'm doubtful if you want to rely on the keydown repeat in your app, since you have no control over repeat interval. You can consider disabling the repeating sounds entirely:
function playSound(e) {
if (e.repeat) return;
// ...
}
Or, if you'd like to keep the repeating, I'd suggest implementing a custom timer for that, giving you the control over how long you want to wait before the next hit.

Change button color based on screen size

What I am trying to achieve is when my device size is less than 736 px, the button should animate. I got the button working correctly, however, I’m struggling to work with the specific screen size.
$(window).resize(function() {
if ($(window).width() <= 736) {
// do something
let myBtn = document.querySelector(".btn");
let btnStatus = false;
myBtn.style.background = "#FF7F00";
function bgChange() {
if (btnStatus == false) {
myBtn.style.background = "#FF0000";
btnStatus = true;
}
else if (btnStatus == true) {
myBtn.style.background = "#FF7F00";
btnStatus = false;
}
}
myBtn.onclick = bgChange;
}
});
.btn {
width: 200px;
height: 100px;
text-align: center;
padding: 40px;
text-decoration: none;
font-size: 20px;
letter-spacing: .6px;
border-radius: 5px;
border: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="btn">CLICK ME</button>
Here's an implementation of what you're trying to do that uses:
class to alter button styling instead of style,
vanilla JavaScript instead of jQuery.
Using class is a good idea, as it keeps the styling in the CSS and out of the JavaScript code.
Using vanilla JavaScript whenever you can is preferable.
Here are the two new classes:
.btn-small-screen {
background: #FF7F00;
}
.btn-clicked {
background: #FF0000;
}
.btn-small-screen class is applied when the window is small, .btn-clicked is toggled whenever the button is clicked.
Here's the JavaScript code:
let myBtn = document.querySelector('.btn');
let isSmallWindow = () => window.innerWidth <= 736;
function toggleButtonOnClick () {
myBtn.classList.toggle('btn-clicked');
}
function setButtonMode () {
if (isSmallWindow()) {
myBtn.classList.add('btn-small-screen');
myBtn.addEventListener('click', toggleButtonOnClick);
} else {
myBtn.classList.remove('btn-small-screen');
myBtn.classList.remove('btn-clicked');
myBtn.removeEventListener('click', toggleButtonOnClick);
}
}
// setup mode on resize
window.addEventListener('resize', setButtonMode);
// setup mode at load
window.addEventListener('load', setButtonMode);
References:
Document.querySelector()
Window.innerWidth
Element.classList
DOMTokenList.toggle()
DOMTokenList.add()
DOMTokenList.remove()
EventTarget.addEventListener()
A working example:
let myBtn = document.querySelector('.btn');
let isSmallWindow = () => window.innerWidth <= 736;
function toggleButtonOnClick () {
myBtn.classList.toggle('btn-clicked');
}
function setButtonMode () {
if (isSmallWindow()) {
myBtn.classList.add('btn-small-screen');
myBtn.addEventListener('click', toggleButtonOnClick);
} else {
myBtn.classList.remove('btn-small-screen');
myBtn.classList.remove('btn-clicked');
myBtn.removeEventListener('click', toggleButtonOnClick);
}
}
// setup small mode on resize
window.addEventListener('resize', setButtonMode);
// setup small mode at load
window.addEventListener('load', setButtonMode);
.btn {
width: 200px;
height: 100px;
text-align: center;
padding: 40px;
text-decoration: none;
font-size: 20px;
letter-spacing: .6px;
border-radius: 5px;
border: none;
}
.btn-small-screen {
background: #FF7F00;
}
.btn-clicked {
background: #FF0000;
}
<button class="btn">CLICK ME</button>
Note: There is one optimization that I left out, so the code would be easier to follow.
Notice that setButtonMode() changes the DOM every time, even though it might already be set to the desired mode. This is inefficient.
To improve efficiency and only change the DOM when necessary, you could introduce a state variable (call it smallMode), and set it true whenever appropriate. Like so:
let smallMode = false;
function setButtonMode () {
if (isSmallWindow()) {
if (!smallMode) {
myBtn.classList.add('btn-small-screen');
myBtn.addEventListener('click', toggleButtonOnClick);
smallMode = true;
}
} else if (smallMode) {
myBtn.classList.remove('btn-small-screen');
myBtn.classList.remove('btn-clicked');
myBtn.removeEventListener('click', toggleButtonOnClick);
smallMode = false;
}
}

Change h1 tag's color globally using javascript

I'm trying to implement a dark mode that activates according to the current time.
That's how I change the body's background color:
if (darkMode) {
document.body.style.backgroundColor = '#31403E';
} else {
document.body.style.backgroundColor = '#F2EDE4';
}
Can I change the h1 tag's color in some kind of global way? Or do I have to check for the property darkMode in every single doc and then assign the proper color.
In modern JavaScript, it could be:
const elements = document.querySelectorAll('h1')
Array.from(elements).forEach(el => el.style.color = '#31403E')
Hope this helps.
You can define a custom CSS stylesheet for dark mode:
#media (prefers-color-scheme: dark) {
color: white;
background: black
}
Where you can define your custom rules accordingly.
There's a nice trick about using filter as well that you can read a bit further over here
You can see this example over here:
https://codepen.io/rikschennink/pen/GLMLj
Where a class .dark-mode is set to the HTML document when toggling using
html.dark-mode {
filter: invert(100%);
img {
filter: invert(100%);
}
}
Docs
first of all they added dark mode preferences in css, they work like so:
#media (prefers-color-scheme: dark) {
h1 {
color: white;
}
}
but a common workaround to this back when we were developing theme switches was adding a "dark" class to the body and doing the following in css
body.dark h1 {
color: white;
}
IF YOU HAVE TO DO IT WITH JAVASCRIPT you have to loop through each and every h1's in the DOM, but it wont work on newly made h1s using javascript since it'll run on page load only as is.
let h1s = document.querySelectorAll("h1"); //gets all the h1s in the page
h1s.forEach( h1 => h1.style.color = "white");
With javascript you can do it like this:
var elements = document.getElementsByTagName("h1");
for(var i = 0; i < elements.length; i++) {
elements[i].style.color = "#000";
}

Categories