It is possible to stop an insertBefore, inside an addEventListener, to add a smooth css, so that the movement produced by the insertion of the div is not abrupt for the user?
I have read many questions, i have tried using settimeout in various ways, without success:
const gallery = document.getElementById('gallery');
const frames = gallery.querySelectorAll('.frame');
for (var i = 0; i < frames.length; ++i) {
frames[i].addEventListener('click', function() {
if (this.className == "frame b") {
//setTimeout( function(){
gallery.insertBefore(this, this.previousElementSibling);
//}, 1000 );
} else {
//setTimeout( function(){
gallery.insertBefore(this, this.previousElementSibling);
//}, 1000 );
};
});
};
.frame {
width: 200px;
height: 100px;
font: bold 400% sans-serif;
color: white;
float: left;
}
.frame.a {
background-color: brown;
}
.frame.b {
background-color: purple;
}
<div id="gallery">
<div class="frame a">A</div>
<div class="frame b">B</div>
</div>
this refers to a different context inside your setTimeout callback; it doesn't refer to the element for which the event is dispatched anymore.
There are a few ways you could do this, here are 3:
Use an arrow function, where this doesn't get bound to a new context:
setTimeout( () => {
gallery.insertBefore(this , this.previousElementSibling);
}, 1000 );
Store a reference to this for use inside the callback:
const _self = this;
setTimeout( function(){
gallery.insertBefore(_self , _self.previousElementSibling);
}, 1000 );
Manually bind the current context to the callback function:
setTimeout( function(){
gallery.insertBefore(this, this.previousElementSibling);
}.bind(this), 1000 );
Another angle I prefer for this is use this wait fn, tidy & concise.
wait fn src
// at the top of the file, or 1000 here and leave out 1000 in the call
const wait = (delay = 300) => new Promise((resolve) => setTimeout(resolve, delay));
// inside
frames[i].addEventListener('click', async () => {
await wait(1000)
if (this.className == "frame b") {
gallery.insertBefore(this, this.previousElementSibling);
} else {
gallery.insertBefore(this, this.previousElementSibling);
};
});
Only trouble is sometimes forgetting the await keyword, hopefully you have an IDE that tells You when you don't need it, like prettier... Won't tell You when you've forgotten though since there are other valid ways to use it. Outer for loop is separate of this question since it's just assigning refs for later.
There is also now
import {setTimeout} from "timers/promises";
await setTimeout(1000);
From Node 16, you don't need the wait fn.
Related
I have the following code:
if(changedStatus()){
var mAns = confirm("You have changed your status. This will alter your estate plan and may cause you to lose some data. Are you sure that you want to do this?");
if(!mAns) return false;
}
This works great with the default JS confirm pop-up, but I would like to implement the same action with HTML5 buttons instead:
<div id="cres_statusmssg">
<p>Changing your status will alter your estate plan and may cause you to lose some data. Do you want to proceed?</p>
<button id="cres_yes">Yes</button>
<button id="cres_cancel">Cancel</button>
I have applied click events to both buttons, but I can't get them to work. Any ideas?
You'll need to understand how to write for asynchrony, since there is no other way to achieve what you want to do. The built-in confirm (and prompt, alert etc) effectively pause JS execution until the user has interacted with the pop-up
Writing nicer pop-ups, you don't have that luxury. You'll need to use callbacks to achieve a working solution
I'd recommend using Promises - which are just glorified callbacks anyway, with the added advantage of being able to be used with async/await - this has the advantage of making the code look more like the code you're used to if you haven't done much asynchronous coding
function statusmssgDialog() {
const dlg = document.getElementById('cres_statusmssg');
dlg.showModal();
return new Promise((resolve, reject) => {
const clk = function(e) {
resolve(this.dataset.value === 'true');
dlg.close();
};
dlg.querySelectorAll('button').forEach(btn =>
btn.addEventListener('click', clk, { once: true})
);
});
}
// usage - it's easier if the function where you
// call statusmssgDialog is `async`
// but you can also use Promise.then/.catch of course
//
async function fn() {
if( true /*changedStatus()*/){
const mAns = await statusmssgDialog();
console.log(`user action is ${mAns}`);
if(!mAns) return mAns;
}
console.log('doing more things here when yes is clicked or chengedStatus is false?');
}
// asynchrony is like a virus,
// anything that calls an asynchronous function and needs the result
// needs to be written to account for asynchrony
// here, I use .then/.catch instead of async/await
// because await isn't an option at the top-level of code
// unless it's the top-level of a module script in modern browsers
fn().then(r => {
if (r === false) {
console.log('cancel was clicked');
return;
}
console.log('yes was clicked');
});
.modal-dialog {
border-radius: 9px;
box-shadow: 0 0 1em rgb(127 127 127 / 0.8);
width: 30rem;
max-width: 90%;
}
.modal-dialog::backdrop {
background: rgb(0 0 0 / 0.4);
backdrop-filter: blur(1px);
}
body {
background-color: hsl(240 100% 95%);
}
<dialog id="cres_statusmssg" class="modal-dialog">
<div>
<p>Changing your status will alter your estate plan and may cause you to lose some data. Do you want to proceed?</p>
<button id="cres_yes" data-value="true">Yes</button>
<button id="cres_cancel" data-value="false">Cancel</button>
</div>
</dialog>
<div>
<p>Dummy page text</p>
<div>
In the above, the dialog promise always resolves, but there is an alternative, to reject if cancel is pressed
function statusmssgDialog() {
const dlg = document.getElementById('cres_statusmssg');
dlg.showModal();
return new Promise((resolve, reject) => {
const clk = function(e) {
dlg.close();
if (this.dataset.value !== 'true') {
return reject('cancel pressed');
}
resolve('yes pressed');
};
dlg.querySelectorAll('button').forEach(btn =>
btn.addEventListener('click', clk, { once: true})
);
});
}
async function fn() {
if( true /*changedStatus()*/) {
try {
const mAns = await statusmssgDialog();
console.log(`yes pressed`);
} catch(e) {
console.log('cancel was pressed');
throw e; // pass the cancel "error" to the caller
}
}
console.log('doing more things here when yes is clicked or chengedStatus is false?');
}
fn().then(r => {
console.log('yes was clicked');
}).catch(e => {
console.log(`user action was ${e}`);
});
.modal-dialog {
border-radius: 9px;
box-shadow: 0 0 1em rgb(127 127 127 / 0.8);
width: 30rem;
max-width: 90%;
}
.modal-dialog::backdrop {
background: rgb(0 0 0 / 0.4);
backdrop-filter: blur(1px);
}
body {
background-color: hsl(240 100% 95%);
}
<dialog id="cres_statusmssg" class="modal-dialog">
<div>
<p>Changing your status will alter your estate plan and may cause you to lose some data. Do you want to proceed?</p>
<button id="cres_yes" data-value="true">Yes</button>
<button id="cres_cancel" data-value="false">Cancel</button>
</div>
</dialog>
<div>
<p>Dummy page text</p>
<div>
Try to use Bootstrap Modal
Using Twitter bootstrap modal instead of confirm dialog
Or jquery ui
How to implement "confirmation" dialog in Jquery UI dialog?
I am using Javascript method Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
Is there any way I can get to know when the scroll is over. Say there was an animation, or I have set {behavior: smooth}.
I am assuming scrolling is async and want to know if there is any callback like mechanism to it.
There is no scrollEnd event, but you can listen for the scroll event and check if it is still scrolling the window:
var scrollTimeout;
addEventListener('scroll', function(e) {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(function() {
console.log('Scroll ended');
}, 100);
});
2022 Update:
The CSS specs recently included the overscroll and scrollend proposal, this proposal adds a few CSS overscroll attributes, and more importantly to us, a scrollend event.
Browsers are still working on implementing it. (It's already available in Chromium under the Web Platforms Experiments flag.)
We can feature-detect it by simply looking for
if (window.onscrollend !== undefined) {
// we have a scrollend event
}
While waiting for implementations everywhere, the remaining of this answer is still useful if you want to build a polyfill:
For this "smooth" behavior, all the specs say[said] is
When a user agent is to perform a smooth scroll of a scrolling box box to position, it must update the scroll position of box in a user-agent-defined fashion over a user-agent-defined amount of time.
(emphasis mine)
So not only is there no single event that will fire once it's completed, but we can't even assume any stabilized behavior between different browsers.
And indeed, current Firefox and Chrome already differ in their behavior:
Firefox seems to have a fixed duration set, and whatever the distance to scroll is, it will do it in this fixed duration ( ~500ms )
Chrome on the other hand will use a speed, that is, the duration of the operation will vary based on the distance to scroll, with an hard-limit of 3s.
So this already disqualifies all the timeout based solutions for this problem.
Now, one of the answers here has proposed to use an IntersectionObserver, which is not a too bad solution, but which is not too portable, and doesn't take the inline and block options into account.
So the best might actually be to check regularly if we did stop scrolling. To do this in a non-invasive way, we can start an requestAnimationFrame powered loop, so that our checks are performed only once per frame.
Here one such implementation, which will return a Promise that will get resolved once the scroll operation has finished.
Note: This code misses a way to check if the operation succeeded, since if an other scroll operation happens on the page, all current ones are cancelled, but I'll leave this as an exercise for the reader.
const buttons = [ ...document.querySelectorAll( 'button' ) ];
document.addEventListener( 'click', ({ target }) => {
// handle delegated event
target = target.closest('button');
if( !target ) { return; }
// find where to go next
const next_index = (buttons.indexOf(target) + 1) % buttons.length;
const next_btn = buttons[next_index];
const block_type = target.dataset.block;
// make it red
document.body.classList.add( 'scrolling' );
smoothScroll( next_btn, { block: block_type })
.then( () => {
// remove the red
document.body.classList.remove( 'scrolling' );
} )
});
/*
*
* Promised based scrollIntoView( { behavior: 'smooth' } )
* #param { Element } elem
** ::An Element on which we'll call scrollIntoView
* #param { object } [options]
** ::An optional scrollIntoViewOptions dictionary
* #return { Promise } (void)
** ::Resolves when the scrolling ends
*
*/
function smoothScroll( elem, options ) {
return new Promise( (resolve) => {
if( !( elem instanceof Element ) ) {
throw new TypeError( 'Argument 1 must be an Element' );
}
let same = 0; // a counter
let lastPos = null; // last known Y position
// pass the user defined options along with our default
const scrollOptions = Object.assign( { behavior: 'smooth' }, options );
// let's begin
elem.scrollIntoView( scrollOptions );
requestAnimationFrame( check );
// this function will be called every painting frame
// for the duration of the smooth scroll operation
function check() {
// check our current position
const newPos = elem.getBoundingClientRect().top;
if( newPos === lastPos ) { // same as previous
if(same ++ > 2) { // if it's more than two frames
/* #todo: verify it succeeded
* if(isAtCorrectPosition(elem, options) {
* resolve();
* } else {
* reject();
* }
* return;
*/
return resolve(); // we've come to an halt
}
}
else {
same = 0; // reset our counter
lastPos = newPos; // remember our current position
}
// check again next painting frame
requestAnimationFrame(check);
}
});
}
p {
height: 400vh;
width: 5px;
background: repeat 0 0 / 5px 10px
linear-gradient(to bottom, black 50%, white 50%);
}
body.scrolling {
background: red;
}
<button data-block="center">scroll to next button <code>block:center</code></button>
<p></p>
<button data-block="start">scroll to next button <code>block:start</code></button>
<p></p>
<button data-block="nearest">scroll to next button <code>block:nearest</code></button>
<p></p>
<button>scroll to top</button>
You can use IntersectionObserver, check if element .isIntersecting at IntersectionObserver callback function
const element = document.getElementById("box");
const intersectionObserver = new IntersectionObserver((entries) => {
let [entry] = entries;
if (entry.isIntersecting) {
setTimeout(() => alert(`${entry.target.id} is visible`), 100)
}
});
// start observing
intersectionObserver.observe(element);
element.scrollIntoView({behavior: "smooth"});
body {
height: calc(100vh * 2);
}
#box {
position: relative;
top:500px;
}
<div id="box">
box
</div>
I stumbled across this question as I wanted to focus a particular input after the scrolling is done (so that I keep the smooth scrolling).
If you have the same usecase as me, you don't actually need to wait for the scroll to be finished to focus your input, you can simply disable the scrolling of focus.
Here is how it's done:
window.scrollTo({ top: 0, behavior: "smooth" });
myInput.focus({ preventScroll: true });
cf: https://github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932
Btw this particular issue (of waiting for scroll to finish before executing an action) is discussed in CSSWG GitHub here: https://github.com/w3c/csswg-drafts/issues/3744
Solution that work for me with rxjs
lang: Typescript
scrollToElementRef(
element: HTMLElement,
options?: ScrollIntoViewOptions,
emitFinish = false,
): void | Promise<boolean> {
element.scrollIntoView(options);
if (emitFinish) {
return fromEvent(window, 'scroll')
.pipe(debounceTime(100), first(), mapTo(true)).toPromise();
}
}
Usage:
const element = document.getElementById('ELEM_ID');
scrollToElementRef(elment, {behavior: 'smooth'}, true).then(() => {
// scroll finished do something
})
These answers above leave the event handler in place even after the scrolling is done (so that if the user scrolls, their method keeps getting called). They also don't notify you if there's no scrolling required. Here's a slightly better answer:
$("#mybtn").click(function() {
$('html, body').animate({
scrollTop: $("div").offset().top
}, 2000);
$("div").html("Scrolling...");
callWhenScrollCompleted(() => {
$("div").html("Scrolling is completed!");
});
});
// Wait for scrolling to stop.
function callWhenScrollCompleted(callback, checkTimeout = 200, parentElement = $(window)) {
const scrollTimeoutFunction = () => {
// Scrolling is complete
parentElement.off("scroll");
callback();
};
let scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
parentElement.on("scroll", () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
});
}
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>
i'm not an expert in javascript but i made this with jQuery. i hope it helps
$("#mybtn").click(function() {
$('html, body').animate({
scrollTop: $("div").offset().top
}, 2000);
});
$( window ).scroll(function() {
$("div").html("scrolling");
if($(window).scrollTop() == $("div").offset().top) {
$("div").html("Ended");
}
})
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>
I recently needed callback method of element.scrollIntoView(). So tried to use the Krzysztof Podlaski's answer.
But I could not use it as is. I modified a little.
import { fromEvent, lastValueFrom } from 'rxjs';
import { debounceTime, first, mapTo } from 'rxjs/operators';
/**
* This function allows to get a callback for the scrolling end
*/
const scrollToElementRef = (parentEle, childEle, options) => {
// If parentEle.scrollTop is 0, the parentEle element does not emit 'scroll' event. So below is needed.
if (parentEle.scrollTop === 0) return Promise.resolve(1);
childEle.scrollIntoView(options);
return lastValueFrom(
fromEvent(parentEle, 'scroll').pipe(
debounceTime(100),
first(),
mapTo(true)
)
);
};
How to use
scrollToElementRef(
scrollableContainerEle,
childrenEle,
{
behavior: 'smooth',
block: 'end',
inline: 'nearest',
}
).then(() => {
// Do whatever you want ;)
});
Why doesn't the background change right as I copy? I added a console.log() and as you can see, the console.log() works, but the background won't change. What is the problem here?
To test, click on the snippet and then press CMD + C
(Windows:CTRL + C)
window.addEventListener('copy', function(e) {
e.preventDefault();
//This should change!
document.getElementById("object").style.backgroundColor = 'white';
console.log("Started!");
tryCopyAsync(e).then(() =>
document.getElementById("object").style.backgroundColor = 'gray'
);
});
async function tryCopyAsync(e){
if(navigator.clipboard){
await e.clipboardData.setData('text/plain',getText());
}
}
function getText(){
var html = '';
var row = '<div></div>';
for (i=0; i<100000; i++) {
html += row;
}
return html;
}
#object{
width:100%;
height:100vh;
background:gray;
}
body{
padding:0;
margin:0;
overflow:hidden;
}
<div id='object'></div>
First, your sleepFor method is completely blocking the event-loop synchronously, even if it is called from an async function:
window.addEventListener('copy', function(e) {
e.preventDefault();
//This should change!
document.getElementById("object").style.backgroundColor = 'white';
console.log("Started!");
tryCopyAsync(e).then(() =>
document.getElementById("object").style.backgroundColor = 'gray'
);
console.log('sync');
});
async function tryCopyAsync(e){
if(navigator.clipboard){
await e.clipboardData.setData('text/plain',getText());
}
}
function sleepFor(sleepDuration){
var now = new Date().getTime();
while(new Date().getTime() < now + sleepDuration){}
}
function getText(){
console.log('blocking');
var html = '';
var row = '<div></div>';
for (i=0; i<10; i++) {
html += row;
sleepFor(300);
}
console.log('stopped blocking');
return html;
}
onclick = e => document.execCommand('copy');
#object{
width:100%;
height:100vh;
background:gray;
}
body{
padding:0;
margin:0;
overflow:hidden;
}
click to trigger the function
<div id='object'></div>
But even if it were called in a microtask, that wouldn't change a thing, because microtasks also do block the event-loop. (Read that linked answer, it explains how the rendering is tied to the event-loop).
If what you want is to have your code let the browser do its repaints, you need to let the browser actually loop the event-loop, and the only ways to do this are:
split your getText logic and make it wait for the next event-loop iteration by posting a task (e.g through setTimeout)
use a dedicated Worker to produce the data returned by getText.
However beware you were not using the async Clipboard API, but simply overriding the default value of the copy event, which can not be done asynchronously. So going this way you will actually need to really use the Clipboard API.
Here is an example using a MessageChannel to post a task since current stable Chrome still has a 1ms minimum delay for setTimeout:
window.addEventListener('copy', function(e) {
e.preventDefault();
//This should change!
document.getElementById("object").style.backgroundColor = 'white';
console.log("Started!");
tryCopyAsync(e).then(() =>
document.getElementById("object").style.backgroundColor = 'gray'
);
});
async function tryCopyAsync(e) {
if (navigator.clipboard) { // you were not using the Clipboard API here
navigator.clipboard.writeText(await getText());
}
}
async function getText() {
var html = '';
var row = '<div></div>';
for (i = 0; i < 1000000; i++) {
if (i % 1000 === 0) { // proceed by batches of 1000
await waitNextFrame();
}
html += row;
}
return html;
}
function waitNextFrame() {
return new Promise(postTask);
}
function postTask(task) {
const channel = postTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", () => task(), {
once: true
});
channel.port2.postMessage("");
channel.port1.start();
}
onclick = (evt) => document.execCommand("copy");
#object {
width: 100%;
height: 100vh;
background: gray;
}
body {
padding: 0;
margin: 0;
overflow: hidden;
}
<div id='object'></div>
Well, firstly, the argument for "then" should be a function.
.then(()=>{})
From running your code, it looks like this
document.getElementById("object").style.backgroundColor = 'gray';
is getting called immediately after setting the background color white, so you aren't able to notice it turning white, even though it is (just very very briefly).
Try setting some logging in your tryCopyAsync function to see why it is finishing too quickly.
The following code is intended to display a 0, then a few seconds later change this to 1, then back to 0, and so on, ad infinitum.
The problem is this (obviously) freezes the webpage itself. I could use an animated gif but want to expand this further and code becomes the better choice.
How might I do this?
<div id="bit" style="width: 100px; height: 100px; margin: auto; border: 1px solid #000; text-align: center;"></div>
<script>
var bitValue = 0;
for (;;) {
setTimeout(showBit(), 2000);
}
function showBit() {
document.getElementById("bit").innerHTML = bitValue;
bitValue = Math.abs(bitValue - 1);
}
</script>
A few things went wrong:
setTimeout(showBit(), 1000);
must be:
setTimeout(showBit, 100);
as you want to pass a function and not execute it immeadiately. Another thing is that you cannot just do
for(;;) { /*...*/ }
as that blocks the browser forever. You would have to do it asynchronously:
const delay = ms => new Promise(res => setTimeout(res, ms));
(async function() {
while(true) {
await delay(1000);
showBit();
}
})();
Or a bit simpler with a pseudorecursive timeout:
(function next() {
showBit();
setTimeout(next, 1000);
})();
Or if you dont want to do that manually, just use setInterval:
setInterval(showBit, 1000);
Like #CertainPerformance said, setInterval should be used instead. Here are a few good examples of how it can be used - https://www.w3schools.com/jsref/met_win_setinterval.asp
Use **setInterval()** with **Math.round** and **Math.random**?
var time = setInterval(f, 2000);
function f() {
document.getElementById("bit").innerHTML = Math.round(Math.random());
}
Use if
var time = setInterval(function() {
if (document.getElementById("bit").innerHTML == 0) {
document.getElementById("bit").innerHTML = 1;
}
else {
document.getElementById("bit").innerHTML = 0;
}
}, 2000);
How can I trigger a function when the browser window stops scrolling? Either by mouse wheel, click, space bar or arrow key? Is there any event for such action? I have tried to search online but couldn't get any solution. I'm fine with a jQuery solution.
There's no "event" but you could make your own, like this:
$(function() {
var timer;
$(window).scroll(function() {
clearTimeout(timer);
timer = setTimeout(function() {
$(window).trigger("scrollStop");
}, 250);
});
});
Then you could bind to it, like this:
$(window).bind("scrollStop", function() {
alert("No one has scrolled me in 250ms, where's the love?");
});
This creates an event, there's no "stop" for this, but you can define your own... in this case "stop" is defined as "hasn't scrolled in 250ms", you can adjust the timer to your liking, but that's the idea.
Also, if you're just doing one thing there's no need for an event, just put your code where I'm calling $(window).trigger("scrollStop") and it'll run n milliseconds after the scroll stops.
The Non-jQuery Javascript version of the chosen answer:
var elem = document.getElementById('test');
(() => {
var onScrollStop = (evt) => {
// you have scroll event as evt (for any additional info)
var scrollStopEvt = new CustomEvent('scrolling-stopped', {detail: 'foobar stopped :)'});
elem.dispatchEvent(scrollStopEvt);
}
var scrollStopLag = 300 // the duration to wait before onScrollStop is triggerred.
var timerID = 0;
const handleScroll = (evt) => {
clearInterval(timerID);
timerID = setTimeout(
() => onScrollStop(evt),
scrollStopLag
)
}
elem.addEventListener('scroll', handleScroll);
})()
elem.addEventListener(
'scrolling-stopped',
(evt) => console.log(evt.detail)
)
#test {
height: 300px;
width: 300px;
overflow: auto;
}
#test #test-inner {
height: 3000px;
background: linear-gradient(lightseagreen 0%, lightyellow 40%, lightcoral 100%);
}
<h4>Scroll inside the green box below:</h4>
<div id="test">
<div id="test-inner"></div>
</div>
Good Luck...
P.S. I am a BIG fan of jQuery :)