I have a very large and complicated jQuery plugin which I will avoid posting here for simplicity's sake. My problem is very simple, and I will reduce it only to the relevant code:
I have a click event attached to a set of buttons:
$("ul.tick-boxes button").click(function(event) {
event.preventDefault();
$("ul.product-grid").addClass("loading");
$(this).toggleClass("active");
$theElement.trigger("filterOptionsChanged");
});
If you go to this link, you can see these in action in the left sidebar:
http://mazer.com/collections/refrigerator?preview_theme_id=22019779
Here is the css that produces a check-mark when you click the buttons:
ul.tick-boxes button.active .tick-box::after {
content: "\e603";
font-family: "custom-icons";
color: #51425d;
top: 2px;
left: 2px;
font-size: 0.75rem;
position: absolute;
}
If your computer is as slow as mine, then when you click these filter options, it takes a second or so to "tick" the "tick-box". If you can't see it, try unticking it, which for me takes noticeably longer. The time-point where the "tick" visibly renders is always simultaneous with the product-grid rebuilding and rendering. I haven't posted the code for manipulating the product-grid, but you can know that the line $theElement.trigger("filterOptionsChanged") triggers a lot of array and object processing to build a document fragment of the new product list, and updates the DOM at the end. I understand this can take a second, that is not my problem. But what I don't understand is why my "tick-boxes" are waiting until after the code of that event is finished to render. According to my css, all I need is a class active on the button, and that code is fired one line above triggering the "filterOptionsChanged" event, so it should fire before any product grid changes happen.
Now. If I open up my inspector in chrome, I can actually see the active classes toggling instantaneously on click, before the product grid updates. However, the css which adds the tick doesn't catch the active class on the element until after my "filterOptionsChanged" code completes.
My first attempt to solve the problem will be posted below. I read a good bit about the "expensiveness" of css pseudo-selectors. That essentially to a browser, it is like a dom manipulation every time an ::after element is created. So I then write this css:
ul.tick-boxes button .tick-box::after {
content: "\e603";
font-family: "custom-icons";
color: #51425d;
top: 2px;
left: 2px;
font-size: 0.75rem;
position: absolute;
opacity: 0;
}
ul.tick-boxes button.active .tick-box::after {
opacity: 1;
}
So now an ::after element always exists from the beginning, any rendering costs are paid for at the outset, so my thought was, now when I click this button, the tick is already there, we are just giving it an opacity of 1. Didn't fix the delay.
Now, I try removing the "filterOptionsChanged" event trigger entirely. This makes my whole sorting plugin stop working, but I don't care at this point, because I want to understand what is causing the problem. When I do remove that event trigger, the buttons and css render snappy. No more problems.
I have a vague thought that, ok, if a click event can be snappy without that event trigger, I need a way of seperating the two. First add the active class, then trigger "filterOptionsChanged". I think, ok, jQuery Deferreds. Here is that code:
$("ul.tick-boxes button").click(function(event) {
event.preventDefault();
var showLoading = jQuery.Deferred();
$("ul.product-grid").addClass("loading");
$(this).toggleClass("active");
showLoading.resolve();
$.when(showLoading).done(function() {
$theElement.trigger("filterOptionsChanged");
});
});
So showLoading is a blank Deferred, I then add my classes for the tick boxes to show, then I resolve the deferred. Now I say, when showLoading is done, then do the whole product-grid manipulation. Don't do these at the same time, javascript, wait for one to finish, then do the other. Still no avail. Any ideas?
According to this, all function calls in JavaScript block the UI until they complete; I'd wager that this includes the original function call to start the click event. A cheap solution might be to replace the trigger with something like:
setTimeout(function() { $theElement.trigger("filterOptionsChanged"); }, 200);
Which will hopefully delay the trigger long enough for the browser to repaint the UI (you could/should add a little loading icon in the original function, then remove it in the timeout). You could also take a look at web workers, which look like they're pretty much threads.
Try this,
CSS:
ul.tick-boxes button.active .tick-box::after {
content: "\e603";
font-family: "custom-icons";
color: #51425d;
top: 2px;
left: 2px;
font-size: 0.75rem;
position: absolute;
}
JS:
$("ul.tick-boxes button").click(function(event) {
event.preventDefault();
$("ul.product-grid").addClass("loading");
$(this).toggleClass("active").fadeIn(10, function () {
$theElement.trigger("filterOptionsChanged");
});
});
Using callback to delay the triggering. See if this work for you.
Related
Setup
I have a Mobile User Interface with its own UI Back Button in the bottom-left corner of the viewport.
N.B. In this case, the UI is an overlay which appears above the web-page being visited, so as much as I'm a fan of History.pushState(), in this
case, I have no specific intention to maintain a navigable history of
overlay views using History.pushState().
If the user has progressed into the UI-overlay by two or more views, then, whenever the UI Back Button is clicked or tapped, the UI-overlay should display the previous view.
Issue
I noticed my UI Back Button can be pretty sensitive and it's easy to inadvertently interact with it such that the EventListener thinks it has been tapped twice or even three times, when the user-intention is simply to tap it once.
Proposed Solution
Evidently some kind of debouncing is needed, as we might more commonly use with an onscroll or an onresize event.
I have written debouncers before using named setTimeout() functions and using clearTimeout() to repeatedly cancel the named setTimeout(), so that only one scroll or resize event (the very last one) actually fires.
But a debouncer with repeated clearTimeout() functions feels over-elaborate in this context.
Further Issues
Ideally, I am looking for a simple, quick, easy, non-verbose way to momentarily deactivate the EventListener after the first click has been detected.
I could use removeEventListener() - but this requires, in every deployment, specific knowledge of the format of the original addEventListener()
I could use AbortSignal - but this requires signal to be set in the options parameter of the original addEventListener() and I can't count on that
Is there a simple and generic approach to debouncing onclick events?
There is an unexpectedly simple way to debounce an onclick event, requiring only two lines of javascript.
The approach sets the CSS pointer-events property of the EventTarget to none before resetting it to auto moments later:
const myFunction = (e) => {
e.target.style.setProperty('pointer-events', 'none');
setTimeout(() => {e.target.style.setProperty('pointer-events', 'auto')}, 800);
// REST OF FUNCTION HERE
}
Working Example:
const myElement = document.querySelector('.my-element');
const myFunction = (e) => {
e.target.classList.add('unclickable');
setTimeout(() => {e.target.classList.remove('unclickable')}, 2000);
console.log('myElement clicked');
}
myElement.addEventListener('click', myFunction, false);
.my-element {
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
font-family: sans-serif;
font-weight: 900;
color: rgb(255, 255, 255);
background-color: rgb(255, 0, 0);
transition: opacity 0.3s linear;
cursor: pointer;
}
.my-element.unclickable {
opacity: 0.5;
pointer-events: none;
}
<div class="my-element">Click me!</div>
I have a JS function which is triggered on a click event.
At the very first line in the function, I add a class to a html element. The html is a custom loader, and the class just makes the loader visible.
The problem is that the loader does not appear until the script has actually finished executing. The class is set on the element right after the script starts, but nothing visually happens in my browser until the script has finished executing.
$('body').on('click', '#button', function(){
$('#loader').addClass('active');
$('.fields').each(function(i, el){
// does some intensive stuff, including appending elements to a dom fragment, triggering events programmatically. This takes 5-10s to execute.
});
// the loader only appears visible in the browser once the code executed in the loop finishes.
});
The CSS in the active class is:
.active{
z-index: 9999;
display: block;
position: absolute;
left: 40%;
left: calc(50% - 18px);
left: -webkit-calc(50% - 18px);
top: 40%;
}
I tried do re-search this issue, but I couldn't find anything on google.
Maybe it's because I do not know what keywords I should search, I am not sure what is causing or how to refer to this issue.
So guys, you are my last hope. Could you point me in the right direction on how to debug or what is causing this?
If I add the fields loop inside a setTimeout function with 0 timeout, it works. But the idea is to fix it the correct way, and understand why it's not working as expected.
If I add the fields loop inside a setTimeout function with 0 timeout, it works.
This is a sensible approach, although requestAnimationFrame is stylistically slightly nicer.
But the idea is to fix it the correct way, and understand why it's not working as expected.
The browser won't trigger a repaint until the function has finished running - otherwise, it would do a repaint for every individual DOM modification, which would be expensive.
I have a javascript function triggered by a button click. I add a class to an element before going into an 'each' loop.
When running the code in the page, I do not see the change. But if I pause the code in the debugger, I do.
The code is set up like this:
$("#btnApplyDefaults").on('click',
function (e) {
$('#loader').addClass("loading-page");
$('#pricingSheetItems tr').filter(':has(:checkbox:checked)').each(function() {
// Do some stuff
});
$('#loader').removeClass("loading-page");
});
If I run this with data that takes a long time to run, I never see the loading image. But if I have a breakpoint, and walk through I see the image.
Here's the class:
div.loading-page {
position: absolute;
left: 50%;
background-image: url("../../dist/img/loading.gif");
background-repeat: no-repeat;
content: "";
display: block;
width: 32px;
height: 32px;
}
I'm assigning it to this div:
<div tabindex="-1" class="" id="loader"></div>
All of your code runs synchronously, meaning that it completes all of it before your UI responds. You add the loading class at the beginning, do some operations, and remove the class, all before the UI can respond.
If you want your UI to update, you can add a minor pause via setTimeout:
$("#btnApplyDefaults").on('click',
function (e) {
$('#loader').addClass("loading-page");
setTimeout(function () {
$('#pricingSheetItems tr').filter(':has(:checkbox:checked)').each(function() {
// Do some stuff
});
$('#loader').removeClass("loading-page");
}, 0);
});
This should show the loading class. I suspect it won't animate though, because your page will be busy doing computations. You should look into using WebWorkers or some sort of asynchronous worker for a better user experience.
In my backbone.js application, I'm trying to fade in the view element after it's been appended. However it doesn't work.
Live example here: http://metropolis.pagodabox.com
var itemRender = view.render().el;
$('#items-list').append(itemRender);
$(itemRender).addClass('show');
However if I add a small setTimeout function, it works.
var itemRender = view.render().el;
$('#items-list').append(itemRender);
setTimeout(function(){
$(itemRender).addClass('show');
},10);
Using fadeIn() also works but I prefer to use straight CSS for the transition as it's more efficient, and prefer not to use any setTimeout "hacks" to force it to work. Is there a callback I can use for append? Or any suggestions? The full code is below:
itemRender: function (item) {
var view = new app.ItemView({ model: item }),
itemName = item.get('name'),
itemRender = view.render().el;
$('#items-list').append(itemRender);
$(itemRender).addClass('show');
app.itemExists(itemName);
}
CSS/LESS:
#items-list li {
padding: 0 10px;
margin: 0 10px 10px;
border: 1px solid #black;
.border-radius(10px);
position: relative;
.opacity(0);
.transition(opacity)
}
#items-list li.show {.opacity(1)}
This "hack" you mention (or some variant of it) is occasionally necessary for web development, simply due to the nature of how browsers render pages.
(NOTE: This is all from memory, so while the overall idea is right please take any details with a small grain of salt.)
Let's say you do the following:
$('#someElement').css('backgroundColor', 'red');
$('#someElement').css('backgroundColor', 'blue');
You might expect to see the background color of #someElement flash red for a brief moment, then turn blue right? However, that won't happen, because browsers try to optimize rendering performance by only rendering the final state at the end of the JS execution. As a result, the red background will never even appear on the page; all you'll ever see is the blue.
Similarly here, the difference between:
append
set class
and:
append
wait 1ms for the JS execution to finish
set class
Is that the latter allows the element to enter the page and AFTER the JS is executed have its style change, while the former just applies the style change before the element gets shown.
So while in general window.setTimeout should be avoided, when you need to deal with these ... complications of browser rendeering, it's really the only way to go. Personally I like using the Underscore library's defer function:
var itemRender = view.render().el;
$('#items-list').append(itemRender);
_(function(){
$(itemRender).addClass('show');
}).defer();
It's the same darn thing, but because it's encapsulated in a library function it feels less dirty to me :-) (and if the "post-render" logic is more than a line or two I can factor it in to a Backbone View method and do _(this.postRender).defer() inside my render method).
You can use CSS animations
#keyframes show {
0% { opacity: 0; }
100% { opacity: 1; }
}
#items-list li {
padding: 0 10px;
margin: 0 10px 10px;
border: 1px solid #black;
.border-radius(10px);
position: relative;
}
#items-list li.show {
animation: show 1s;
}
I’ve already spent hours looking at as many online resources and stackoverflow questions as I can find but for some reason I just can’t figure this out.
I’m attempting to use CSS and image sprites to make a link display as an image that changes once it is hovered over and once it has been clicked. I’ve played round with CSS and looked at JavaScript for far too long now and I just need some direction on how to get it working.
I’ve managed to make it change once its hovered over however what i really need to do is have the image change once it is clicked. So the begin with it displays the play button and when its clicked it displays a pause button, click it again and it displays the play button etc.
From what i can gather i will need to use JavaScript and an onclick event. However I’m not sure how this would work or how to use it with image sprites.
My CSS so far looks like this
.testclass .stratus {
background-position: -1px -1px;
width: 21px;
height: 21px;}.
.testclass .stratus:hover {background-position: -1px -29px; width: 21px; height:
21px;}.
However this only effects the play button and when it is hovered over. Now i need a way to display the pause button when the play button is clicked and vice versa.
Image sprites URL.
http://www.danceyrselfclean.com/wp-content/uploads/2012/12/sprites.png
URL of page im trying to get this to work on.
http://www.priceofmilk.co.uk/uncategorized/ddd-2
Can this be achieved using CSS and HTML or will I also need to use some JavaScript? Any assistance would be much appreciated.
I made a simple example. I use background colors and an anchor but you can easy implement this in your situation.
update
Updated the code so it uses your images.
HTML
<a class="play_pause">PLAY</a>
CSS
.play_pause {
display: block;
width: 24px;
height: 23px;
text-indent: -99999px;
background: url(http://www.danceyrselfclean.com/wp-content/uploads/2012/12/sprites.png);
cursor: pointer;
}
.playing {
background-position: -27px 0;
}
.playing:hover {
background-position: -27px -28px !important;
}
.play_pause:hover {
background-position: 0 -28px;
}
And the JS:
$(document).ready(function() {
$(".play_pause").click(function() {
$(this).toggleClass('playing');
});
});
JsFiddle example
If you only wanted to detect the first click, you could do this in pure CSS by giving the link an id and using the :target pseudoclass (e.g. a#theid:target {...})
But since you need to detect a second click, you'll need to use JS to toggle between CSS classes. The basic way is to use an event handler:
document.getElementById('theid').onclick = function(){
this.className = this.className=='play' ? 'pause' : 'play';
};
You will have to use JavaScript to accomplish the switching, there is no way to accomplish such logic with pure CSS.
The easiest way to go would be to have two classes play and pause. Through CSS you declare which part of the sprite you want to show for each of those classes. Then you attach a click-event listener to the element with JavaScript, and in the click-event callback you remove class play from the element and apply class pause instead, and vice versa.
MDN has a good article on how to attach event-listeners to an element. And this SO question discuss how you can add/remove classes on an element.
That is simple where have you read?
jQuery('.testclass .stratus').click(function{
jQuery(this).toggleClass('played');
})
css:
.played{
background-position: -1px -29px;
}
Example using .querySelectorAll and .addEventListener, with your current sprite. No jQuery is used.
var elm = document.querySelectorAll('.testclass .stratus'), i = elm.length, e;
while (e = elm[--i])
e.addEventListener('click', function () { // this fn generates the listener
var pause = false; // captured in scope, not global
return function () { // this is the actual listener
this.style.backgroundPositionX = (pause = !pause)?'-20px':'0px';
}
}(), false); // the () invokes the generator