Hiding 3 elements in sequence not working using JavaScript "transitionend" event? - javascript

WHAT. I have 3 boxes: blue, red and black and I'm trying to transition them one after another, using JavaScript event listeners, in the following order: black, red and then blue.
HOW: https://codepen.io/gremo/pen/XWVeQVP?editors=1010
Basic HTML structure and nesting (pen for the complete example):
<div id="container">
<div id="blue"></div>
<div id="red">
<div id="black"></div>
</div>
</div>
JavaScript code is very simple:
const blue = document.getElementById('blue');
const red = document.getElementById('red');
const black = document.getElementById('black');
const hideElement = element => {
console.log(`Hiding element ${element.getAttribute('id')}`);
element.classList.add(element.dataset.hideClass);
};
black.addEventListener('transitionend', () => hideElement(red)); // when black transition ends, hide the red
red.addEventListener('transitionend', () => hideElement(blue)); // when red transition ends, hide the blue
hideElement(black); // hide the black (start the chain)
THE PROBLEM: after the black ends, blue and red start at the same time... and this is wrong because blue should start hiding after red completes. In addition, console show that the transitionend event listener for blue is called 2 times.
Any help is much appreciated, I've be struggling with this problem since days.

It seems that because black is a child of red, the transitionend event got bubbled up, causing red to fire the event twice:
document.addEventListener('DOMContentLoaded', () => {
const blue = document.getElementById('blue');
const red = document.getElementById('red');
const black = document.getElementById('black');
const hideElement = element => {
console.log(`Hiding element ${element.getAttribute('id')}`);
element.classList.add(element.dataset.hideClass);
};
black.addEventListener('transitionend', (e) => {
e.stopPropagation();// <-- added this line
hideElement(red);
});
red.addEventListener('transitionend', () => hideElement(blue));
hideElement(black);
});
(Edited from your codepen; I can't fork it because I'm too lazy to register)

transitionend event will trigger all the listeners once executed. As red and black both register the event listener before the first event triggers both a run. To prevent this, you could either pass a function which will add the event listener inside the hide element function or more preferably pass the event alongside the element and call stopPropagation. stopPropagation is an event function which will prevent the propagation of the event, thus only triggering the first one in this case
e?.stopPropagation()
Below is a modified function which should work in your context
document.addEventListener('DOMContentLoaded', () => {
const blue = document.getElementById('blue');
const red = document.getElementById('red');
const black = document.getElementById('black');
const hideElement = (element,e) => {
console.log(`Hiding element ${element.getAttribute('id')}`);
e?.stopPropagation()
element.classList.add(element.dataset.hideClass);
};
black.addEventListener('transitionend', (e) => hideElement(red,e));
red.addEventListener('transitionend', (e) => hideElement(blue,e));
hideElement(black);
});

Related

addEventListener only works when I use window infront of it

I would like to use a code when I press the space bar a shape appears and it disappears when I press it again. I'm trying to get the addEventListener to work with a sample:
hello = document.querySelector('#Player');
with player being the id of the shape that I want to control. I declared hello above and initialized it in setup (I am using JavaScript), the Player id has also been initialized in HTML and given a shape in CSS. When I use
hello.addEventListener('keypress', (event) => {
console.log(event.key)
})
nothing happens, but when I use
window.addEventListener('keypress', (event) => {
console.log(event.key)
})
it works. Is there anything that I am doing wrong?
It is because by default div is not selectable. In order to make it selectable you need to use tabindex attribute on your div. It will make your div selectable.
const hello = document.querySelector('#player');
hello.addEventListener("keypress", evt => {
console.log(evt.key)
})
<div id="player" tabindex="0">
Player Shape
</div>
It will show a boundary around your div which can be remove by using css -
outline: none;
That's what should happen.
You can add the keypress event to the window or document.
And, if you add it to both, the window wins over for some reason – someone else might clarify this to both of us.
const el = document.getElementById("el");
el.addEventListener("keypress", event => keyPressed(event, "blue"));
document.addEventListener("keypress", event => keyPressed(event, "green"));
window.addEventListener("keypress", event => keyPressed(event, "purple"));
function keyPressed(event, color) {
if (event.key = " ")
el.style.backgroundColor = color;
}
#el {
width: 100px;
height: 100px;
background: red;
}
<div id="el"></div>

Arrow flip function semi working, almost complete JS

I have created a function that allows a user to click a visible div that drops down a "hidden" div/submenu. To the right of the visible div there is an arrow image. In total there are 5 visible divs and 5 arrows. I am needed to have the arrows rotate 180 when the "hidden" div is opened, and rotate back to 0 deg when closed. With the current code written, I am able to select the first arrow image to rotate. If I am to click on the second div, which I would like the second arrow to rotate, the first arrow is getting the script. I would believe to use querySelectorAll, but the console does not pick it up. Any tips are always greatly appreciated!
//
const questionBox = document.getElementsByClassName("question__container");
const arrows = document.querySelector(".question__container--img");
[...questionBox].forEach((el) =>
el.addEventListener("click", (event) => {
const subMenu = event.target.parentElement.parentElement.querySelector(
".options__container"
);
subMenu.classList.toggle("open");
if (subMenu.classList.contains("open")) {
arrows.style.transform = "rotate(180deg)";
} else {
arrows.style.transform = "rotate(0deg)";
}
})
);
//
Building on your current logic, here's what you could do.
Get all arrows with querySelectorAll
Create a function collapseAllArrows that basically loops through all arrows and collapses them.
When any of the div element is clicked and the subMenu for that element is toggled to open then call the collapseAllArrows function to collapse all expanded arrows.
The find the arrow for the clicked div element and expand it.
Your code should look something like this
const questionBox = document.getElementsByClassName("question__container");
const arrows = document.querySelectorAll(".question__container--img");
const collapseAllArrows = () => {
arrows.forEach(arrow => arrow.style.transform = "rotate(0deg)";)
}
[...questionBox].forEach((el) =>
el.addEventListener("click", (event) => {
const subMenu = event.target.parentElement.parentElement.querySelector(
".options__container"
);
subMenu.classList.toggle("open");
if (subMenu.classList.contains("open")) {
collapseAllArrows();
const currentArrow = el.querySelector(".question__container--img");
currentArrow.style.transform = "rotate(180deg)";
}
})
);

How can I trigger a css animation multiple times using javascript and react in an optimal way?

I created a react component that is made of a 3d cube (using css) and 2 buttons that rotate this cube 90 degrees (1 button rotates the cube left and the other one right)
import React from 'react';
import './App.css';
function App() {
const rotateLeft = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
const newNode = boxArea.cloneNode(true);
boxArea.classList.add('rotateLeft');
setTimeout(function(){
boxArea.parentNode.replaceChild(newNode, boxArea);
}, 3005)
};
const rotateRight = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
const newNode = boxArea.cloneNode(true);
boxArea.classList.add('rotateRight');
setTimeout(function(){
boxArea.parentNode.replaceChild(newNode, boxArea);
}, 3005)
};
return (
<div className="App">
<div className="hexagon-panel">
<div className="wrapper">
<div className="box-area">
<div className="box front"/>
<div className="box back"/>
<div className="box left"/>
<div className="box right"/>
</div>
</div>
<div className="hexagon-actions">
<button onClick={rotateLeft}>Left</button>
<button onClick={rotateRight}>Right</button>
</div>
</div>
</div>
);
}
export default App;
As you can see, I trigger the animation by adding the class, I wait a bit more than 3 seconds (the animation lasts 3 seconds) and then I replace the Node with one that doesn't contain the class.
A better solution (but I don't like it either) is to remove the class after the animation ends.
const rotateLeft = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
boxArea.classList.add('rotateLeft');
setTimeout(function(){
boxArea.classList.remove('rotateLeft');
}, 4000)
};
But I am not convinced. The reason for this is the setTimeout since it is a global function I am afraid of a memory leak (even if it is a small one). Calling a new setTimeout on every click and not erasing the previous one.
I was thinking about this
const rotateLeft = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
boxArea.classList.add('rotateLeft');
const func = setTimeout(function(){
boxArea.classList.remove('rotateLeft');
}, 4000)
clearTimeout(func);
};
But I don't know if that even makes sense.
Is there a way a better way to remove the class without the use of setTimeout? Or in the case that it is needed, what would be a good practice to delete it?
Thank you for your time!
You might be looking for this: Using Animation Events. It includes the animationend event that fires when the animation ends.
Or in the future, you could use the Web Animations API once it is widely implemented.
Or if you want to stick with setTimeout, just put the cleanup code (clearTimeout, remove class, etc.) before starting the animation. This way, you always get a 'clean' state to work with.

Replace an img src on scroll

I am trying to replace logo-text-black src attribute so that the svg img changes as the user scrolls. Is it possible to add this to my current script?
img/logo-text-white.svg // Top State
img/logo-text-black.svg // Scroll State
HTML
<nav class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
<img class="logo" src="img/logo.svg">
<a href="#top"><img class="logo-text" src="img/logo-text-white.svg">
</a>
</div>
</nav>
JS
$(window).scroll(function() {
var value = $(this).scrollTop();
if (value > 100)
$(".navbar-default").css("background", "white"); // Scroll State
else
$(".navbar-default").css("background", "transparent"); // Top state
});
To replace image source you may use jQuery .attr method:
var initialSrc = "img/logo.svg";
var scrollSrc = "img/logo-text-black.svg";
$(window).scroll(function() {
var value = $(this).scrollTop();
if (value > 100)
$(".logo").attr("src", scrollSrc);
else
$(".logo").attr("src", initialSrc);
});
This approach requires only one <img> with logo class in the HTML:
<nav class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
<img class="logo" src="img/logo.svg">
</div>
</nav>
Ignoring the fact that the simple answer to the question asked is that you use the .attr function to change an attribute for an element when using jQuery, this is how I would go about accomplishing the task set forth in your question.
First, I would put all of this in a function (mainly to separate the variables and logic from other page scripts to prevent interference).
My next bit of advice would be to implement the background color change in two or more CSS classes. This has the benefit of simplifying the JavaScript, as well as keeping the styling part in the styling area.
Next, I like to make constant variables for my "magic words", so that if I change the word used later on I only have to change the word once in the code, instead of everywhere the word is used.
// cache the magic words
const DARK = 'dark';
const LIGHT = 'light';
I would put the image sources into an object where the keys are the magic words associated with those sources. This allows for quick and convenient lookup later.
// define our different sources for easy access later
const sources = {
light: "http://via.placeholder.com/150x50/fff/000?text=logo",
dark: "http://via.placeholder.com/150x50/000/fff?text=logo"
};
After that I would pre-load the images to prevent a visual delay the first time the source is changed.
// pre-load the images to prevent jank
document.body.insertAdjacentHTML('beforeend', `
<div style="display: none!important">
<img src="${ sources[LIGHT] }">
<img src="${ sources[DARK] }">
</div>
`);
It is important to note that performing tasks on-scroll can cause problems.
The main problems are:
The effects can be blocking, which means that process heavy tasks will cause "scroll jank". This is where there is a visual inconsistency with how the page scrolls.
It is possible for the scroll event to fire while there is already a scroll event listener executing. This may cause the two executions to interfere with each other.
Combatting these problems is easy:
To prevent scroll-jank, wrap the handler in a setTimeout call. This will move the execution of the handler to the top of the stack to be executed at the next earliest convenience.
To prevent multiple handlers from running simultaneously, define a "state" variable outside of the handler to keep track of execution state.
This variable will be set to true when an event handler is executing and false when there is no event handler execution. When the handler execution begins, check the value of the state variable:
If it is true, cancel the execution of this handler call.
If it is false, set the state to true and continue.
Just make sure that wherever you may be exiting the function, you also reset the state variable.
// define our scroll handler
const scroll_handler = _ => setTimeout(_ => {
// if we are already handling a scroll event, we don't want to handle this one.
if (scrolling) return;
scrolling = true;
// determine which theme should be shown based on scroll position
const new_theme = document.documentElement.scrollTop > 100 ? DARK : LIGHT;
// if the current theme is the theme that should be shown, cancel execution
if (new_theme === theme) {
scrolling = false;
return;
}
// change the values
logo.src = sources[new_theme];
el.classList.remove(theme);
el.classList.add(new_theme);
// update the state variables with the current state
theme = new_theme;
scrolling = false;
});
After that, just assign the event listener.
Here it is all together:
function navbarSwitcher(el) {
// cache the reference to the logo element for use later
const logo = el.querySelector('.logo');
// cache the magic words
const DARK = 'dark';
const LIGHT = 'light'
// define our state variables
let scrolling = false;
let theme = LIGHT;
// define our different sources for easy access later
const sources = {
light: "http://via.placeholder.com/150x50/fff/000?text=logo",
dark: "http://via.placeholder.com/150x50/000/fff?text=logo"
};
// pre-load the images to prevent jank
document.body.insertAdjacentHTML('beforeend', `
<div style="display: none!important">
<img src="${ sources[LIGHT] }">
<img src="${ sources[DARK] }">
</div>
`);
// define our scroll handler
const scroll_handler = _ => setTimeout(_ => {
// if we are already handling a scroll event, we don't want to handle this one.
if (scrolling) return;
scrolling = true;
// determine which theme should be shown based on scroll position
const new_theme = document.documentElement.scrollTop > 100 ? DARK : LIGHT;
// if the current theme is the theme that should be shown, cancel execution
if (new_theme === theme) {
scrolling = false;
return;
}
// change the values
logo.src = sources[new_theme];
el.classList.remove(theme);
el.classList.add(new_theme);
// update the state variables with the current state
theme = new_theme;
scrolling = false;
});
// assign the event listener to the window
window.addEventListener('scroll', scroll_handler);
}
// attach our new plugin to the element
navbarSwitcher(document.querySelector('.wrap'));
body {
height: 200vh;
}
.wrap {
width: 100%;
position: fixed;
}
.wrap.light {
background-color: white;
}
.wrap.dark {
background-color: black;
}
<div class="wrap light">
<img class="logo" src="http://via.placeholder.com/150x50/fff/000?text=logo">
</div>

ARCGIS Javascript vertex custom right click event possible?

i am using the ARCGIS Javascript API and trying to override the default right click behavior of the vertex points of a shape.
in ESRI's help it does list the onVertexClick event however from here it seems there is no way to determine if this is a right or left click event so i cannot override just the rightclick.
https://developers.arcgis.com/javascript/jsapi/edit.html
I am trying to set the right click behavour to just delete the current node/vertex instead of showing a menu with the option Delete.
EDIT
Here is the current event that exists within the ARCGIS api.
this.eventsList.push(dojo.connect(this._editToolbar, 'onVertexClick', $.proxy(this.addCustomVertexClickEvent, this)));
this event is already in the api however it does not return any way for me to determine left/right click.
your comment "listen for the click event then test the button attribute of the MouseEvent object" would work however i cant actually add a click event to the vertex points directly as these are inside the ARCGIS api code.
For anyone else who is looking for a way to do this without hacking around. You can listen to "contextmenu" (right click) events on the body, set a flag in the "contextmenu" handler to let the application know the current state. Simulate a click event to the "vertex handle" with a "mousedown", "mouseup" combination. In the "vertex-click" handler check for the right click flag set in the "contextmenu" handler
var editToolbar = new Edit(map, options);
var rightClick;
$('body').on('contextmenu', function(e) {
var target = e.target;
if(target.tagName === 'circle') {
// We only care about this event if it targeted a vertex
// which is visualized with an SVG circle element
// Set flag for right click
rightClick = true;
// Simulate click on vertex to allow esri vertex-click
// to fill in the data for us
var mouseDownEvt = new MouseEvent('mousedown', e.originalEvent);
target.dispatchEvent(mouseDownEvt);
var mouseUpEvt = new MouseEvent('mouseup', e.originalEvent);
target.dispatchEvent(mouseUpEvt);
// Since this event will be handled by us lets prevent default
// and stop propagation so the browser context menu doesnt appear
e.preventDefault();
e.stopPropagation();
}
});
editToolbar.on('vertex-click', function(e) {
if(rightClick) {
// Handle the right click on a vertex
rightClick = null;
}
});
after hearing back from ESRI it seems they do not provide this detail in their API so this is not possible yet.
I ended up doing this differently. I wanted to add a UI so the user could enter the XY of the point
// setup to allow editing
this.editToolbar = new EditToolbar(this.map, { allowDeleteVertices: false });
const rcMenuForGraphics = new RightClickVertexContextMenu();
const menu = rcMenuForGraphics.createMenu();
// bind to the map graphics as this is where the vertex editor is
this.map.graphics.on("mouse-over", (evt)=> {
// bind to the graphic underneath the mouse cursor
menu.bindDomNode(evt.graphic.getDojoShape().getNode());
});
this.map.graphics.on("mouse-out", (evt)=> {
menu.unBindDomNode(evt.graphic.getDojoShape().getNode());
});
this.editToolbar.on("vertex-click", (evt2) => {
rcMenuForGraphics.setCurrentTarget(evt2);
// evt2.vertexinfo.graphic.geometry.setX(evt2.vertexinfo.graphic.geometry.x - 1000);
})
// when the graphics layer is clicked start editing
gl.on("click", (evt: any) => {
this.map.setInfoWindowOnClick(false);
// tslint:disable-next-line: no-bitwise
const t: any = EditToolbar.MOVE | EditToolbar.EDIT_VERTICES;
this.editToolbar.deactivate();
this.editToolbar.activate(t, evt.graphic);
})
The code for the menu uses esri's vertex editor to grab the point, change its XY and then manually call the events to refresh the geometry. Only tested with polygon
import Menu = require("dijit/Menu");
import MenuItem = require("dijit/MenuItem");
import Graphic = require("esri/graphic");
import Edit = require("esri/toolbars/edit");
import Point = require("esri/geometry/Point");
class RightClickVertexContextMenu {
private curentTarget: { graphic: Graphic; vertexinfo: any; target: Edit; };
public createMenu() {
const menuForGraphics = new Menu({});
menuForGraphics.addChild(new MenuItem({
label: "Edit",
onClick: () => {
// this is a bit hooky. We grab the verx mover, change the x/y and then call the _moveStopHandler
console.log(this.curentTarget.vertexinfo);
const e: any = this.curentTarget.target;
const mover = e._vertexEditor._findMover(this.curentTarget.vertexinfo.graphic);
const g: Graphic = mover.graphic;
// add in a UI here to allow the user to set the new value. This just shifts the point to the left
g.setGeometry(new Point(mover.point.x - 1000, mover.point.y ))
e._vertexEditor._moveStopHandler(mover, {dx: 15});
this.curentTarget.target.refresh();
}
}));
menuForGraphics.addChild(new MenuItem({
label: "Delete",
onClick: () => {
// call the vertex delete handler
const ct: any = this.curentTarget.target;
ct._vertexEditor._deleteHandler(this.curentTarget.graphic)
}
}));
return menuForGraphics;
}
public setCurrentTarget(evt: { graphic: Graphic; vertexinfo: any; target: Edit; }) {
this.curentTarget = evt;
}
}
export = RightClickVertexContextMenu;

Categories

Resources