css class gets permanently added to button - javascript

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.

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 to highlight the current section the user is viewing in 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);

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;
}
}

MutationObserver hangs in IE11

I am using MutationObserver to check when some nodes are removed and replaced by other new nodes to an element.
The following code works totally fine in Chrome, but on IE11 it just hangs.
If I change the addedNodes check with removedNodes, it works on IE11. I just don't understand why it hangs when I check for new nodes being added.
Any idea? I can't find any resources for this issue.
var nodeToObserve = document.querySelector('#targetNode');
var callback = function(mutations, observer) {
for (var index = 0; index < mutations.length; index) {
var mutation = mutations[index];
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
console.log(mutation);
break;
}
}
observer.disconnect();
}
const observer = new MutationObserver(callback);
observer.observe(nodeToObserve, {
childList: true, // target node's children
subtree: true // target node's descendants
});
html, body {
height: 100%;
}
#targetNode {
border: 1px solid black;
height: 100%;
}
.childNode {
//height: 20px;
background-color: blue;
margin: 10px;
padding: 10px;
}
.grandChildNode {
height: 20px;
background-color: red;
margin: 10px;
}
<div id="targetNode">
</div>
You aren't incrementing the index in your for loop. Probably the results appear in a different order depending on the browser so the if statement will be triggered on some browsers but not others. Thus, the system will hang when the if statement isn't executed b/c of the infinite loop.

How to sync printing of characters with sound?

I need to emulate what an old manual typewriter does when printing what is being typed on a web page. I want to develop JavaScript functions to pass it a string, and it would print out each character with a delay, and the sound file synced with each letter.
I'm new to JavaScript. What is the preferred method to do this? Should I be looking at jQuery for this? Or is this something simple to do?
I've seen problems with sound files being triggered like this on some web browsers, is there an audio file format which is best for this sort of thing?
I've found this, but the problem is, it doesn't work on all web browsers:
https://rawgit.com/mehaase/js-typewriter/master/example3-typewriter/index.html
You can try something like this:
// The delay between each keystroke
var delay = 300;
// The typewriter sound file url
var clickURL = "https://cdn.rawgit.com/halimb/res/6ffa798d/typewriter.wav";
// Get a reference to the container div
var container = document.getElementById("container");
var sampleString = "Hello world!";
//get a reference to the start button and typewrite onclick
var start = document.getElementById("btn");
start.onclick = function() { typewrite( sampleString ); };
function typewrite( str ) {
var i = 0;
container.innerHTML = "";
type();
function type() {
var click = new Audio( clickURL );
// This is triggered when the browser has enough of the file to play through
click.oncanplaythrough = function() {
click.play();
// Add the character to the container div
container.innerHTML += str[i];
i++;
if(i < str.length) {
window.setTimeout(type, delay);
}
}
}
}
* {
font-family: Courier;
font-size: 32px;
}
.btn {
display: inline-block;
border: 1px solid black;
border-radius: 5px;
padding: 10px;
cursor: pointer;
margin: 10px;
}
<div class="btn" id="btn">Start</div>
<div id="container"></div>
Update: on Safari. It seems the audio has to be triggered by a user event (e.g: onclick..), so I added a button, and made the typewriter start onclick.
The downside is that there's no way to pre-load the audio file, Safari make a server request and downloads the audio each time it is played. the only (dirty) way I could think of to overcome this is to provide a data URI instead of the audioURL.. you can try that if the playback speed really matters (this can be helpful: https://www.ibm.com/developerworks/library/wa-ioshtml5/)

Categories