So I used this totally awesome tool called Visual Event, which shows all the event handlers bound to an object - and I noticed that every time I clicked or played around with my object and checked the list of event handlers bound to it, there were and more every time. My problem is this one: console.trace or stack trace to pinpiont the source of a bug in javascript? After using Visual Event and someone else's suggestion, I'm thinking my problem is that I'm probably binding the same handlers to the same events over and over again. Is there a way to unbind things regularly?
My application has a bunch of plugins connect to dynamically created divs. These divs can be resized and moved around the place. The application is a kind of editor, so users arrange these divs (which contain either images or text) in any design they like. If the user clicks on a div, it becomes "activated", while all other divs on the page get "deactivated". I have a bunch of related plugins, like activateTextBox, initTextBox, deactivateTextBox, readyTextBox, and so on. Whenever a div is first created, the init plugin is called once, just the first time after creation, like so:
$(mydiv).initTextBox();
But readyTextBox and activateTextBox and deactivateTextBox are called often, depending on other user events.
In init, I first use bind things like resizable() and draggable(), then I make the box "ready" for use
$.fn.extend({
initTextBox: function(){
return this.each(function() {
// lots of code that's irrelevant to this question
$this.mouseenter(function(){
if(!$this.hasClass('activated'))
$this.readyTextBox();
}
$this.mouseleave(function(){
if($this.hasClass('ready')){
$this.deactivateTextBox();
$this.click(function(e){
e.preventDefault();
});
}
});
});
});
Here's a simplified summary version of the readyTextBox plugin:
(function($){
$.fn.extend({
readyTextBox: function(){
return this.each(function() {
// lots of code that's irrelevant to this question
$this.resizable({ handles: 'all', alsoResize: img_id});
$this.draggable('enable');
$this.on( "dragstop", function( event, ui )
{/* some function */ });
$this.on("resizestop", function( event, ui ){ /* another function */ });
// and so on
});
Then there's activateTextBox():
$.fn.extend({
activateTextBox: function(){
return this.each(function() {
// lots of code that's irrelevant to this question
$this.resizable('option','disabled',true); //switch of resize & drag
$this.draggable('option', 'disabled', true);
});
Then deactivate, where I turn on draggable and resizable again, using the code:
$this.draggable('enable'); $this.resizable('option','disabled',false);
These divs, or "textboxes" are contained within a bigger div called content, and this is the click code I have in content:
$content.click(function(e){
//some irrelevant code
if( /* condition to decide if a textbox is clicked */)
{ $(".textbox").each(function(){ //deactivate all except this
if($(this).attr('id') != $eparent.attr('id'))
$(this).deactivateTextBox();
});
// now activate this particular textbox
$eparent.activateTextBox();
}
});
This is pretty much the relevant code related to text boxes. Why is it that whenever I drag something around and then check Visual Event, there are more clicks and dragstops and mouseovers than before? Also, the more user interacts with the page, the longer the events take to complete. For example, I mouseout from a div, but the move cursor takes a loooong time to get back to default. I quit dragging, but everything gets stuck for a while before getting ready to take more user clicks, etc. So I'm guessing the problem has to be that I'm binding too many things to the same events need to be unbinding at some point? It gets so bad that draggable eventually stops working at some point. The textboxes just get stuck - they're still able to be resized, but dragging stops working.
Am I binding events over and over
Yes. Have a look at your code:
$this.mouseenter(function(){
…
$this.mouseleave(function(){
…
$this.click(function(e){
…
});
});
});
That means every time you mouseover the element, you add another leave handler. And when you leave the element, every of those handlers adds another click event.
I'm not sure what you want to do, but there are several options:
bind the event handlers only once, and keep track of the current state with boolean variables etc.
before binding, remove all other event handlers that are already bound. jQuery's event namespacing can help you to remove only those which your own plugin added.
use the one() method that automatically unbinds a listener after firing it.
Related
This may be a stupid question. I know I am a little green.
I was set with a task of modifying this old, old system's navigation. There are two nav bars. The second has only search buttons. I was asked to remove the second nav bar, and replace it with a drop down that shows the search functions. I am restricted on what I can change due to the age of this system. There are no restrictions on the JS I can write. They are running jQuery 1.11.1, on an Adobe ColdFusion system (two months ago they upgraded from 1.3.2)
First: when the target is clicked, both the mouseenter and the click event trigger. The mouseenter fires first. This causes a problem on a desktop that is visible to the keen viewer, but on mobile, this creates a horriable usability issue. A: From my understanding mouse events do not happen on a mobile device but do for me. And B: since the mouseenter event runs first, it activates the closeDropDown function before the click event is processed. With the closeDropDown running, its .on('click', f(...eventstuff...)) hears the open click that is intended to trigger the openDropDown function, thus the drop down does not open.
Here are the functions. The console.logs are for checking what runs when.
function openDropDown(){
$('div.dropdown').parent().on('click.open mouseenter', function(event){
$subject = $(this).find('.dropdown-menu')
// console.log(event.type, $subject, "first o");
if(!$subject.is(":visible")){
// console.log($subject, 'second o');
$subject.show()
}else {
if(event.type == 'click'){
// console.log('third o');
$subject.toggle()
}
}
closeDropDown($subject)
// console.log('open complete');
})
}
function closeDropDown($x){
// console.log('first c');
$(document).on("click.close",function(e){
// console.log("second c", e.type, "this type");
if(!$(e.target).closest(".dropdown-menu").parent().length){
// console.log("third c");
if($x.is(":visible")){
// console.log('forth c');
$x.hide()
}
}
$(document).off("click.close")
// console.log('complete close');
})
}
openDropDown()
onSearchClick()
I have read a few posts hoping for some help (like this and that
Over all, I know I need to condense my code. I understand a few ways to fix this (add an if(... are we on a mobile device...) or some counter/check that prevents the closeDropDown from running when the dropdown is closed)
I really want to understand the fundamentals of event listeners and why one runs before the other stuff.
Although suggestions on how to fix this are great, I am looking to understand the fundamentals of what I am doing wrong. Any fundamental pointers are very helpful.
Of note: I just read this: .is(':visible') not working. I will be rewriting the code with out the .is('visible').
Other things that might help:
This is the Chrome Dev Tools console when all my console.log(s) are active.
First, click after page load....
Drop down opens and quickly closes.
Second click....
Thanks! All your help is appreciated!
This is a pretty broad question. I'll try to be terse. I don't think ColdFusion should be tagged here, because it seems like it only has to do with HTML/CSS/JS.
Configuring Events
First, I'd like to address the way you have your script configured.
You'd probably benefit from looking at the event handling examples from jquery.
Most people will create events like the following. It just says that on a click for any document element with the ID of "alerter", run the alert function.
// Method 1
$(document).on(click, "#alerter", function(event){
alert("Hi!");
});
OR
// Method 2
$(document).on("click", "#alerter", ClickAlerter);
function ClickAlerter(event) {
alert("Hi!");
}
Both methods are totally valid. However, it is my opinion that the second method is more readable and maintainable. It separates event delegation from logic.
For your code, I would highly recommend removing the mixing of event assignment and logic. (It removes at least one layer of nesting).
Incidentally, your event listeners don't appear to be configured correctly. See the correct syntax and this example from jQuery.
$( "#dataTable tbody" ).on( "click", "tr", function() {
console.log( $( this ).text() );
});
Regarding Multiple Events
If you have multiple event listeners on an object, then they will be fired in the order which they are registered. This SO question already covers this and provides an example.
However, this doesn't mean that a click will occur before a mouseenter. Because your mouse has to literally enter the element to be able to click it, the event for mouseenter is going to be fired first. In other words, you have at least 2 factors at play when thinking about the order of events.
The order in which the browser will fire the events
The order in which they were registered
Because of this, there isn't really such a thing as "simultaneous" events, per se. Events are fired when the browser wants to fire them, and they will go through events and fire the matches in the order that you assigned them.
You always have the option of preventDefault and stopPropagation on these kinds of events if you want to alter the default event behavior. That will stop the browser's default action, and prevent the event from bubbling up to parent elements, respectively.
Regarding Mobile Mouse Events
Mouse events absolutely happen on mobile devices, and it's not safe to assume they don't. This article covers in great depth the scope of events that get fired. To quote:
"[Y]ou have to be careful when designing more advanced touch interactions: when the user uses a mouse it will respond via a click event, but when the user touches the screen both touch and click events will occur. For a single click the order of events is:
touchstart
touchmove
touchend
mouseover
mousemove
mousedown
mouseup
click
I think you would benefit from reading that article. It covers common problems and concepts regarding events in mobile and non-mobile environments. Again, a relevant statement about your situation:
Interestingly enough, though, the CSS :hover pseudoclass CAN be triggered by touch interfaces in some cases - tapping an element makes it :active while the finger is down, and it also acquires the :hover state. (With Internet Explorer, the :hover is only in effect while the user’s finger is down - other browsers keep the :hover in effect until the next tap or mouse move.)
An Example
I took all these concepts and made an example on jsFiddle to show you some of these things in action. Basically, I'm detecting whether the user is using a touchscreen by listening for the touchstart event and handling the click differently in that case. Because I don't have your HTML, I had to make a primitive interface. These are the directives:
We need to determine if the user has a touchscreen
When the user hovers over the button, the menu should appear
On a mobile device, when a user taps the button, the menu should appear
We need to close the menu when the user clicks outside of the button
Leaving the button should close the menu (mobile or otherwise)
As you will see, I created all my events in one place:
$(document).on("mouseover", "#open", $app.mouseOver);
$(document).on("mouseout", "#open", $app.mouseOut);
$(document).on("click", "#open", $app.click);
$(document).on("touchstart", $app.handleTouch);
$(document).on("touchstart", "#open", $app.click);
I also created an object to wrap all the logic in, $app which gives us greater flexibility and readability down the road. Here's a fragment of it:
var $app = $app || {};
$app = {
hasTouchScreen: false,
handleTouch:function(e){
// fires on the touchstart event
$app.hasTouchScreen = true;
$("#hasTouchScreen").html("true");
$(document).off("touchstart", $app.handleTouch);
},
click: function(e) {
// fires when a click event occurrs on the button
if ($app.hasTouchScreen) {
e.stopPropagation();
e.preventDefault();
return;
}
// since we don't have a touchscreen, close on click.
$app.toggleMenu(true);
},
touch: function(e) {
// fires when a touchstart event occurs on the button
if ($("#menu").hasClass("showing")) {
$app.toggleMenu(true);
} else {
$app.toggleMenu();
}
}
};
Problem Fiddle:
Click To View (Attempt #1, using Delegated Events)
Click To View (Attempt #2, brute force approach as below)
Click To View (Attempt #3, refactored, has problem I am trying to solve)
On a project I'm working with, I'm exploring a rather dynamic form. In addition to some static elements, there are various interactive elements, which can be cloned from a hidden 'template' markup set and added at various points in the business process.
Because of the dynamic nature, my tried-and-true method of setting up a jQuery element cache and event handlers on-load, then letting the user do whatever isn't working out, because of this dynamic nature; I was finding that my dynamically-added elements had no click events.
To solve this problem, I manually set up a rebind method for each scripted element in question. The rebind process involves A) re-acquiring the set of elements for a given descriptive selector, B) dropping any existing events on that cache, as those events apply to an incomplete element set, and C) calling a bind method to apply the new events to the entire set.
The brute-force, working way that I got, had this going on:
var $elementCache = $('.some-class');
function rebindSomeLink() {
// Re-acquire the element cache...
$elementCache = $('.some-class');
// Drop all existing events on the cache...
$elementCache.unbind();
// Call a bind function to establish new events.
bindSomeLink();
}
function bindSomeLink() {
$elementCache.click(function (e) {
// ...Behavior...
});
}
// There are four other links with a similar rebind/bind function relationship set up.
Naturally, I seized on the rebind being repeated so often with nearly the exact same code - ripe for a refactor. We have a common library namespace, where I added a rebindEvents function...
var MyCommon = function () {
var pub = {};
pub.rebindEvents = function($elementCache, selector, bindFunction) {
$elementCache = $(selector);
$elementCache.unbind();
bindFunction();
};
return pub;
}();
Upon trying to call that, and run the site, I immediately stubbed my toe on an UncaughtTypeError: method click cannot be called on object undefined.
As it turns out, it seems when I call the following:
MyCommon.rebindEvents($elementCache, '.some-class', bindSomeLink);
The $elementCache is not being passed to the rebindEvents method; when I step to it in my debugger, $elementCache inside of rebindEvents is undefined.
Some handy StackOverflow research revealed to me that JavaScript does not have referential-passing, at least in the C/C++/C# sense that I am familiar with, which leads me to my two Questions:
A) Is it even possible for me to refactor this rebind functionality with a cache reference pass of some sort?
B) If it's possible for me to refactor my rebind function to my common namespace, how would I go about doing it?
Use Jquery On to bind events at an element high-up in the DOM that is always present.
http://api.jquery.com/on/
This is the simplest way to handle event binding to dynamically created elements.
here is a jsfiddle that shows an example.
$('#temp').on('click', 'button', function(){
alert('clicked');
});
$('#temp').append('<button>OK</button>');
The event is bound to a div, which later has a button dynamically added. Because the button has no click event, the event "bubbles" up the DOM tree to its parent element which does have a click event for a button, so it handles it and the event "bubbles" no further.
OK I am lost here. I have read numerous postings here and else where on the topic of how to check for the state of a given element in particular whether it is visible or hidden and make the change of state trigger an event. But I cannot make any of the suggested solutions work.
Firstly, as every one seems to leap on this point first, the need to do this has arisen as I have one jQuery script which deals with displaying an svg icon in a clickable state. And another which already has functions to perform relevant actions when the form is made visible by clicking the icon and obviously I want to reuse these.
What I have tried:
Initially I tried have both the scripts acting on a single click event (this remains the ideal solution)....
Script 1:
$(".booked").on("click", function() {
$("#cancellation_Form_Wrapper").css("visibility","visible");
}).svg({loadURL: '../_public/_icons/booked.svg'});
Script 2:
$(".booked").on("click", function() {
// do stuff
});
This did not work so I tried to research sharing an event across two scripts but couldn't make any head way on this subject so I tried triggering another event for the second script to pick up....
Script 1:
$(".booked").on("click", function() {
$("#cancellation_Form_Wrapper").css("visibility","visible");
$("#cancellation_Form_Wrapper").trigger("change");
}).svg({loadURL: '../_public/_icons/booked.svg'});
Script 2
$("#cancellation_Form_Wrapper").on("change", function(event){
// do stuff
});
This did not work again I am unclear why this didn't have the desired effect.
Then I tried is(:visible) ....
Script 1
$(".booked").on("click", function() {
$("#cancellation_Form_Wrapper").css("visibility","visible");
}).svg({loadURL: '../_public/_icons/booked.svg'});
Script 2
$("#cancellation_Form_Wrapper").is(":visible", function(){
// do stuff
});
So I am a bit lost. The ideal would be to return to the first notion. I do not understand why the click event on the svg cannot be handled by both scripts. I assume that this has something to do with event handlers but I am not sure how I could modify the script so they both picked up the same event.
Failing that I could use the fact the visibility state changes to trigger the action.
Any guidance welcomed.
Edit Ok I have just resolved the issue with script 2 picking up the triggered event from script 1. Sad to say this was a basic error on my part ... the form was preventing the display of the alert. However I still cannot get the is(:visible) to work.
Your syntax may be off:
$("#cancellation_Form_Wrapper").is(":visible", function(){
// do stuff
});
should be:
if($("#cancellation_Form_Wrapper").is(":visible")){
// do stuff
});
EDIT: If you want something to happen after a div is made visible, you may want to use the show() callback rather than toggling visibility:
$('#cancellation_Form_Wrapper').show(timeInMilliseconds, function(){
// do something
});
However, this needs to take place in the same function, which I don't think improves your position.
The problem is probably that your second on() script is firing at the same time as your first, meaning the wrapper is not yet visible. You could try wrapping the second on() in a short timeout:
$(".booked").on("click", function() {
setTimeout(function(){
if($("#cancellation_Form_Wrapper").is(":visible")){
// do stuff
});
}, 100);
});
That should introduce enough of a delay to make sure the wrapper has been shown before trying to execute the second statement.
I've discovered a resource leak on a webpage I'm working on.
This webpage has two textfields, that upon click show a modal dialog, perform a data request to the backend, and then present that information in a table that the user can select an entry from for use in the textbox they originally clicked.
I'm binding the click events to the textboxes like so:
var $field = $('#one-of-the-text-fields');
$field.click(function () {
App.DialogClass.show();
App.DialogClass.PopulateTable();
App.DialogClass.GotoPageButtonAction(actionArgs); // Offender!
});
...Which calls...
App.DialogClass = (function($) {
var pub {},
$gotoPage = $('#pageNumberNavigationField'),
$gotoPageButton = $('#pageNumberNavigationButton');
// ...SNIP unimportant other details...
pub.GotoPageButtonAction = function (args) {
$gotoPageButton.click(function () {
var pageNumber = $gotoPage.val();
pub.PopulateTable(args); // Breakpoint inserted here...
});
};
return pub;
})(jQuery);
I noticed the leak because when I ran through using Chrome's JavaScript debugger, I'm always having one extra breakpoint hit every time I click a different button (e.g. the first time I click field A, the breakpoint is hit twice. When I hit field B after that, the break point is hit three times. If I click A after that, the breakpoint is hit four times. Extrapolate as necessary.)
Nowhere in my code am I doing anything about an existing click event for a given field. I suspect my leak stems from the fact that the events are not getting cleaned up. That being said, I am also not terribly familiar with JavaScript/jQuery. What are some techniques for removing click events from a control?
Sure. Just unbind them:
$field.unbind('click');
However, bear in mind that this will remove all event handlers for click, not just yours. For safety, you should use namespaces when binding handlers:
$field.bind('click.mynamespace', function(){
// do something
});
Then,
$field.unbind('click.mynamespace');
So, then, only your handler will be removed.
If you have used .bind() to bind them .unbind() removes the events
If you have used .on() to bind them .off() removes the events
JQuery offers the unbind function to unbind event listeners.
Note that you may also do it in vanilla JS using removeEventListener.
But instead of unbinding, you probably should not bind each time in GotoPageButtonAction : once is enough.
Heres my link:
http://tinyurl.com/6j727e
If you click on the link in test.php, it opens in a modal box which is using the jquery 'facebox' script.
I'm trying to act upon a click event in this box, and if you view source of test.php you'll see where I'm trying to loacte the link within the modal box.
$('#facebox .hero-link').click(alert('click!'));
However, it doesn't detect a click and oddly enough the click event runs when the page loads.
The close button DOES however have a click event built in that closes the box, and I suspect my home-grown click event is being prevented somehow, but I can't figure it out.
Can anyone help? Typically its the very last part of a project and its holding me up, as is always the way ;)
First, the reason you're getting the alert on document load is because the #click method takes a function as an argument. Instead, you passed it the return value of alert, which immediately shows the alert dialog and returns null.
The reason the event binding isn't working is because at the time of document load, #facebox .hero-link does not yet exist. I think you have two options that will help you fix this.
Option 1) Bind the click event only after the facebox is revealed. Something like:
$(document).bind('reveal.facebox', function() {
$('#facebox .hero-link').click(function() { alert('click!'); });
});
Option 2) Look into using the jQuery Live Query Plugin
Live Query utilizes the power of jQuery selectors by binding events or firing callbacks for matched elements auto-magically, even after the page has been loaded and the DOM updated.
jQuery Live Query will automatically bind the click event when it recognizes that Facebox modified the DOM. You should then only need to write this:
$('#facebox .hero-link').click(function() { alert('click!'); });
Alternatively use event delegation
This basically hooks events to containers rather than every element and queries the event.target in the container event.
It has multiple benefits in that you reduce the code noise (no need to rebind) it also is easier on browser memory (less events bound in the dom)
Quick example here
jQuery plugin for easy event delegation
P.S event delegation is pencilled to be in the next release (1.3) coming very soon.