Why I need 2 requestAnimationFrame invokes to wait the dom rendered - javascript

I'm trying to locate which element newly rendered is under mouse pointer. (*)
Here is my code:
btn.addEventListener('click', function () {
btn.remove();
for (let i = 0; i < 10; i++) {
lst.appendChild(document.createElement('li')).textContent = 'Element ' + i;
}
requestAnimationFrame(function () { requestAnimationFrame(function () {
const chosen = document.querySelector('li:hover');
alert(chosen && 'Your mouse on ' + chosen.textContent); // do something more with chosen
}); });
});
#btn { width: 200px; height: 200px; }
#lst { width: 200px; line-height: 20px; display: block; padding: 0; }
#lst li { display: block; height: 20px; width: 200px; overflow: hidden; }
#lst li:hover { background: #ccc; }
<button id=btn>Click Me</button>
<ul id=lst><ul>
I'm confused that I need 2 requestAnimationFrame to make my code execute correctly. Removing one raf, the alert will show null instead.
The code also seems ugly to me. How to implement it more elegantly?
In case anyone care about: I'm running my code on Firefox. And the code, as a part of my Firefox extension, only need to target to Firefox 60+.
(*): The story behind may be more complex. But to keep it simple...

That's quite an interesting behavior you found here, browsers seem to not update the :hover before that second frame, even if we force a reflow or what else.
Even worse, in Chrome if you hide the <button> element using display:none, it will stay the :hover element until the mouse moves (while normally display:none elements are not accessible to :hover).
The specs don't go into much details about how :hover should be calculated, so it's a bit hard to tell it's a "bug" per se.
Anyway, for what you want, the best is to find that element through the document.elementsFromPoints method, which will work synchronously.
btn.addEventListener('click', function ( evt ) {
btn.remove();
for (let i = 0; i < 10; i++) {
lst.appendChild(document.createElement('li')).textContent = 'Element ' + i;
}
const chosen = document.elementsFromPoint( evt.clientX, evt.clientY )
.filter( (elem) => elem.matches( "li" ) )[ 0 ];
alert(chosen && 'Your mouse on ' + chosen.textContent); // do something more with chosen
});
#btn { width: 200px; height: 200px; }
#lst { width: 200px; line-height: 20px; display: block; padding: 0; }
#lst li { display: block; height: 20px; width: 200px; overflow: hidden; }
#lst li:hover { background: #ccc; }
<button id=btn>Click Me</button>
<ul id=lst><ul>

I cannot exactly answer the question why you need 2 rafs.
But i can provide you an more elegant way with async / await. Create a small function called nextTick that returns an promise. So you await for the next frame.
So you can first wait till the button is gone, create your elemens, then await again for the next painting cycle to be sure the elements are accessible
btn.addEventListener('click', async function () {
btn.remove();
await nextTick();
for (let i = 0; i < 10; i++) {
lst.appendChild(document.createElement('li')).textContent = 'Element ' + i;
}
await nextTick()
const chosen = document.querySelector('li:hover');
alert(chosen && 'Your mouse on ' + chosen.textContent); // do something more with chosen
});
function nextTick() {
return new Promise(requestAnimationFrame)
}
#btn { width: 200px; height: 200px; }
#lst { width: 200px; line-height: 20px; display: block; padding: 0; }
#lst li { display: block; height: 20px; width: 200px; overflow: hidden; }
#lst li:hover { background: #ccc; }
<button id=btn>Click Me</button>
<ul id=lst><ul>

Related

Constraining block displacement using transform translate, mousemove and pageX (pageY)

I am resizing and positioning a box using the mousemove event. Those. i change transform translate and width (height) with pageX (pageY). But due to the fact that the mouse event mousemove does not always have time to be processed (for example, if you move the mouse quickly) or does not have time to read conditions, the block goes out of bounds.
Question: what do I need to do in this case so that the block does not go beyond the boundaries?
This is how it looks roughly. Those. in this example, the second_block is outside the first_block (500px), i.e. it does not have time to read the condition. How should this issue be resolved? Also for convenience https://jsfiddle.net/ManuOP/t1r4szdx/3/
<div id="first_block" class="first_block">
<div id="auxiliary_block">
<div id="second_block" class="second_block"></div>
<input id="point" class="point" name="name_point" type="button">
</div>
</div>
<script src="1.block_in_center_question.js"></script>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
div {
display: flex;
flex-direction: column;
}
div.first_block {
height: 300px;
width: 500px;
background: green;
}
div#auxiliary_block {
position: absolute;
}
div.second_block {
height: 200px;
width: 300px;
background: orange;
}
input.point {
position: absolute;
cursor: pointer;
height: 14px;
width: 14px;
border: none;
background: black;
right: -7px;
top: 50%;
}
"use strict";
let second_block = document.getElementById('second_block');
let point = document.getElementById('point');
function change_second_block() {
if(second_block.clientWidth < 500) {
second_block.style.width = `${start_x + event.pageX}px`;
}
}
point.addEventListener('mousedown', (event) => {
window.start_x = second_block.clientWidth - event.pageX;
document.addEventListener('mousemove', change_second_block);
});
You could just test the new width and if it's too large then constrain it to be no more than the maximum.
This snippet does this for the x direction and forces it to remain at or below 500px.
"use strict";
let second_block = document.getElementById('second_block');
let point = document.getElementById('point');
function change_second_block() {
if (second_block.clientWidth < 500) {
second_block.style.width = (start_x + event.pageX) < 500 ? `${start_x + event.pageX}px` : '500px';
}
}
point.addEventListener('mousedown', (event) => {
window.start_x = second_block.clientWidth - event.pageX;
document.addEventListener('mousemove', change_second_block);
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
div {
display: flex;
flex-direction: column;
}
div.first_block {
height: 300px;
width: 500px;
background: green;
}
div#auxiliary_block {
position: absolute;
}
div.second_block {
height: 200px;
width: 300px;
background: orange;
}
input.point {
position: absolute;
cursor: pointer;
height: 14px;
width: 14px;
border: none;
background: black;
right: -7px;
top: 50%;
}
<div id="first_block" class="first_block">
<div id="auxiliary_block">
<div id="second_block" class="second_block"></div>
<input id="point" class="point" name="name_point" type="button">
</div>
</div>
You can work around this issue if the size of the change in the item is above a certain limit, or by checking the limit and stopping the update. I prevented the overflow caused by rapid mouse movement by updating its code as follows:
function change_second_block()
{
console.log("Event.PageX: " + event.pageX);
if(event.pageX < 500 )
{
if(second_block.clientWidth < 500)
{
second_block.style.width = `${start_x + event.pageX}px`;
}
}
}
References
Javascript mouse event not captured properly when mouse moved very fast

Using getBoundingClientRect() when resizing the window

I have this navbar and everytime I click an option in the navbar the absolute positioned indicator gets the position of the option on the left and the width with the help of getBoundingClientRect() and it is moved to the target.
The problem is when I resize the window the indicator changes it's position and moves away.To stay in the same place when I resize the window I applied an eventListener to the window and everytime is resized I get the new values of left and width with getBoundingClientRect().
It works but I wonder if that is a bad way to do it because of the calculations that happen everytime the window is resized and if that is the case what is a better way to do this.
Here is the code:
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.getBoundingClientRect().left}px`;
navigator.style.width = `${firstOption.getBoundingClientRect().width}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.getBoundingClientRect().left}px`;
navigator.style.width = `${e.target.getBoundingClientRect().width}px`;
};
});
window.addEventListener('resize', function() {
let navOptionActive = nav.querySelector('.nav-option-active');
navigator.style.left = `${navOptionActive.getBoundingClientRect().left}px`;
navigator.style.width = `${navOptionActive.getBoundingClientRect().width}px`;
});
* {
margin: 0;
padding: 0;
}
nav {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin: 100px auto;
padding: 7vh 30vw;
width: auto;
background:#eeeeee;
}
.nav-option {
padding: 0 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
You can make your <nav> element tightly wrap the buttons, then position the underline relative to the <nav>. A new wrapper <div> around the <nav> takes care of the margins and gray background. Instead of getBoundingClientRect() you then need to use offsetLeft and offsetWidth.
Note that this doesn't handle the changes in response to your #media query. For that, you could add a resize listener that specifically only handles changes across the 1200px threshold. Alternatively, you could reparent the underline to be a child of the actual nav button while it's not animating. Neither solution is great, but both would get the job done.
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.offsetLeft}px`;
navigator.style.width = `${firstOption.offsetWidth}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.offsetLeft}px`;
navigator.style.width = `${e.target.offsetWidth}px`;
};
});
* {
margin: 0;
padding: 0;
}
.nav-wrapper {
margin: 100px 0;
display: flex;
justify-content: center;
background: #eeeeee;
}
nav {
position: relative;
display: flex;
}
.nav-option {
padding: 7vh 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<div class="nav-wrapper">
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
</div>
If you have to use getBoundingClientRect (which honestly has nothing wrong with it), you can throttle the call, so that only the last resize after sufficient time has passed will execute. There are zillion ways of doing this, I will leave one example:
window.onresize = (function(id = null, delay = 600, oEvent = null){
return function fire(event){
return (new Promise(function(res,rej){
if (id !== null){
oEvent = event;
rej("busy");
return;
}
id = setTimeout(function(){
res(oEvent || event);
},delay);
})).then(function(event){
id = null;
console.log(event, "do getBoundingClientRect call");
}).catch(function(){void(0);});
};
}());
Replace console.log with what you want to do.
Your other option is to switch to intersection observer, if you can restructure your rendering logic. That will require some work

Html,css3, jquery how to create on click event

I want to make a button inside auto generated block to change overflow from hidden to auto.
I created recursive responsive auto-grid in Less, css like this:
.container {
.container-fixed();
[class*='col-'] {
float: right;
width: 100%;
}
.make-grid(#container-xs);
.make-grid(#container-sm);
.make-grid(#container-md);
.make-grid(#container-lg);
}
.container-fixed(#gap: #grid-gap-width) {
margin-right: auto;
margin-left: auto;
padding-left: (#gap / 2);
padding-right: (#gap / 2);
}
.generate-columns(#container-width;
#number-cols;
#i: 1) when (#i =< #number-cols) {
.col-#{i} {
#single-width: #container-width / #number-cols - 0.5;
width: #i * #single-width; // 800px
}
.generate-columns(#container-width;
#number-cols;
#i + 1);
}
.make-grid(#container-width) {
#media(min-width: #container-width) {
width: #container-width;
.generate-columns(#container-width, #grid-c);
}
}
[class*='col-'] {
text-align: center;
overflow: hidden;
height: 250px;
background: #color-h;
display: block;
margin: 1px;
color: #color-text;
position: relative;
}
And now I have long text in HTML inside one of blocks no matter which one, eg. col-9 where is part hidden because I used overflow:hidden;.
What I would like to do is to create a button and on click to change from overflow:hidden; to overflow: auto;.
My question is how to do that, to change from hidden to auto, on click and again to return back to previous state on new click.
I tried something like this but that is not good:
Less - >
[class*='col-'] {
text-align: center;
overflow: hidden;
height: 250px;
background: #color-h;
display: block;
margin: 1px;
color: #color-text;
position: relative;
.show {
overflow: auto;
}
}
JS - >
var content = document.getElementsByClassName("[class*='col-']");
var button = document.getElementbyID("show");
button.onclick = function() {
if (content.className == "show") {
content.className= "";
button.inerHTML = "Read";
} else {
content.className="show";
button.inerHTML = "Close";
}
};
html - >
<div class="col-9">
<a id="button-show">Read</a>
<script src="js/read.js"></script>
<p> some long text ........ </p>
</div>
I hope I am clear enough, what I want to do.
<-- language: lang-javascript -->
$("#button-show").click(function(){
$(".col-9").toggleClass("show")
})
<-- -->
so whenever you click the button, it will add or remove the class show on your elements with the col-9 classnames
You should use .toggle() to toggle the contents between show and hide. Here is an example
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
$(document).ready(function(){
$("#button-show").click(function(){
$("#show").toggle();
});
});
[class*='col-'] {
text-align: center;
overflow: hidden;
height: 250px;
background: #color-h;
display: block;
margin: 1px;
color: #color-text;
position: relative;
}
#show {
overflow: auto;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="col-9">
<a id="button-show">Read</a>
<p id="show"> some long text ........ </p>
</div>
You have multiple problems in your code.
document.getElementsByClassName returns a list of elements (A.K.A. array), so your content.className is wrong as you accesing the array className property (which is non-existant) instead of the className property of each element inside the array. You have to iterate the array and access each element individually. Also, you are not accesing by class, but by selector (There's no class [class*='col-'], but class col-1, col-2, etc...). To select with selectors you have to use querySelector, which selects one element, or querySelectorAll which selects all elements.
Also, to hide an element you don't have to change overflow. overflow is for scrollbars. You have to change the display property to display: none and also as the class show is not a child element, it needs an & character:
[class*='col-'] {
text-align: center;
[...CSS THINGYS...]
position: relative;
&.show { // Note the & before the dot
display: none;
}
}
Your code don't has any jQuery actually. Is plain JS.
Also, the best way to attach events to HTML elements is via addEventListener, so:
var content = document.querySelectorAll("[class*='col-']");
var button = document.getElementbyID("show");
button.addEventListener("click", function() {
var anyShown = false;
content.forEach(function(element) {
if (element.className == "show") {
anyShown = true;
element.className= "";
} else {
element.className="show";
}
});
if (anyShown) {
button.inerHTML = "Read";
} else {
button.inerHTML = "Close";
}
});
If you want it in a more jQuery way you can do this, which do the same as above, but way shorter:
$("#show").on("click", function() {
if ($("[class*='col-']").hasClass("show")) {
$("#show").html("Read");
} else {
$("#show").html("Close");
}
$("[class*='col-']").toggleClass("show");
});
Relevant info:
addEventListener
getelementsbyclassname
querySelector
querySelectorAll
Less "&" operator
jQuery hasClass
jQuery toggleClass
I found solution:
JQ:
$(document).ready(function(){
$("button").click(function(){
$("p3").toggle();
});
});
CSS:
[class*='col-'] {
text-align: center;
overflow:auto;
height: 250px;
background: #color-h;
margin: 1px;
color: #color-text;
position: relative;
.border-radius(10px);
p3 {
margin: 10px ;
padding: 5px;
width: 95%;
text-align: justify;
display: none;
}
}
HTML:
<div class="col-9">
<button>Read</button>
<h1>TITLE</h1>
<p>some tekst.</p>
<p3>Tekst i want to hide ....</p3>
</div>

JavaScript hide on click outside triggers twice from the same click

I have this code:
Full Code:
https://codepen.io/anon/pen/wmLBxW
JavaScript code:
function HideOnClickOutside(aContentElement, aFullScreenElement) {
const outsideClickListener = event => {
event.preventDefault();
let isClickInside = aContentElement.contains(event.target);
if (!isClickInside) {
aFullScreenElement.classList.toggle("hidden", true);
removeClickListener();
}
};
const removeClickListener = () => {
document.removeEventListener("click", outsideClickListener);
};
document.addEventListener("click", outsideClickListener);
}
CSS
#fullscreen {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: auto;
background: black;
opacity: 0.5;
}
.hidden {
display: none;
}
#journal-show-entry {
display: flex;
justify-content: center;
width: 40%;
height: 60%;
border: 1px solid #9b6400;
background-color: #ffffff;
}
HTML
Link
<div id="fullscreen" class="hidden">
<div id="journal-show-entry">
</div>
</div>
Which I found in this thread:
How do I detect a click outside an element?
However just like in my CodePen, it triggers the outsideClickListener on the same click that adds the EventListener making the div hide itself again on the same click and therefore never shows.
Why is this happening?
What is happening is that the event is propagating onto the next layer (the hidden div)
Adding this will fix your problem:
function ShowFullScreenDiv(event) {
event.stopPropagation(); // <-- add this
divFullScreen.classList.toggle("hidden", false);
HideOnClickOutside(divEntry, divFullScreen);
}
And ofcourse add the event to the html:
Link

JS slider image disappears when clicking Prev/Next arrow

I've tried to look for a solution for this but have failed miserably. It's my first ever time using JS (I'm trying to learn) so the possibility of my just not understanding the answers in the search results properly is quite high - sorry about that.
I am wanting a JS carousel, generated from an array, with Prev/Next buttons (ideally responsive etc but that'll come at a later stage), preferably with captions underneath. I can get the carousel to work but I end up getting a text link when I click on either Prev or Next. And I've no idea how to add the caption array underneath (I've taken out the JS for the captions for now because it was messing everything else up even further).
Relevant HTML:
<body onload="changePilt()">
<span id="prev" class="arrow">❮</span>
<div class="karussell" id="karussell">
<img class="karu" name="esislaid">
</div>
<span id="next" class="arrow">❯</span>
<div class="caption">
<h3 name="esikiri"></h3>
</div>
</body>
CSS, just in case:
.karussell {
position: relative;
width: 100%;
max-height: 600px;
overflow: hidden;
}
.arrow {
cursor: pointer;
position: absolute;
top: 40%;
width: auto;
color: #00A7E0;
margin-top: -22px;
padding: 16px;
font-weight: bold;
font-size: 18px;
transition: 0.6s ease;
border-radius: 0 3px 3px 0;
}
#next {
right: 0;
border-radius: 3px 0 0 3px;
}
#prev {
left: 0;
}
.arrow:hover {
background-color: rgba(0,0,0,0.8);
}
.caption {
text-align: center;
color: #00A7E0;
padding: 2px 16px;
}
.karu {
max-width: 75%;
}
#media (max-width:767px){.karu{max-width: 95%;}}
And finally, the dreaded JS:
var i = 0;
var s = 0;
var esileht = [];
var aeg = 5000;
//Image List
esileht[0] = 'img/tooted/raamat/graafvanalinn2016.jpg';
esileht[1] = 'img/tooted/kaart/kaart_taskus_esipool.jpg';
esileht[2] = 'img/tooted/kaart/graafkaart_esikylg.jpg';
//Change Image
function changePilt (){
document.esislaid.src = esileht[i];
if(i < esileht.length -1){
i++;
} else {
i = 0;
}
setTimeout("changePilt()", aeg);
}
document.onload = function() {
}
// Left and Right arrows
//J2rgmine
function jargmine(){
s = s + 1;
s = s % esileht.length;
return esileht [s];
}
//Eelmine
function eelmine(){
if (s === 0) {
s = esileht.length;
}
s = s -1;
return esileht[s];
}
document.getElementById('prev').addEventListener('click', function (e){
document.getElementById('karussell').innerHTML = eelmine();
}
);
document.getElementById('next').addEventListener('click', function (e) {
document.getElementById('karussell').innerHTML = jargmine();
}
);
I'm sure the solution is dreadfully obvious, I just cannot seem to be able to figure it out...
instead of innerHTML change src attribute of image
document.querySelector('#karussell img').src = eelmine();
And
document.querySelector('#karussell img').src = jargmine();

Categories