JavaScript/jQuery: global variable inexplicably being reset, causing menu contraction issue - javascript

http://jsfiddle.net/VhjR7/1/
When you click the my lists menu once, it expands, but if you click it again, it doesn't contract.
The problem is listsExpanded being inexplicably reset to false after it is set properly to true by listsExpand(). This causes the check within $('#mid-wrap').delegate() to inappropriately call listsExpand() again, instead of listsContract() like it should.
I can't figure out where or why this reset is occurring, but I think it has something to do with the sticky light blue menu functionality. Before I started removing and replacing this blue bar after scrolling to fix an IE7 bug, there was no issue with expansion/contraction of the little white menu.
Any ideas on what's causing this?

The issue is that the hover event does not support both function arguments (in and out) when used with .delegate(). You will need to use mouseenter and mouseleave instead of hover.
Change to this:
$('#mid-wrap').delegate('#lists', 'mouseenter', function() {
listsMouseIn = true;
}).delegate('#lists', 'mouseleave', function() {
listsMouseIn = false;
});
FYI, if these HTML objects are static, not added dynamically, you could significantly simplify your code by using direct event handlers on that actual objects rather than .delegate and just stopPropagation() when you've processed the click. Then, you'd see the click first in the object and wouldn't be processing the same click multiple times causing you to need all these global flags to keep track of state.
You could also just use the visibility of the object as your detection mechanism for whether the menu is open/closed too rather than a global variable.

The first part of your hover handler (with listsMouseIn = true;) never actually fires so whenever you click, your $('body').mouseUp() handler assumes that you are not hovering the lists button, and therefore hides the menu just for the $('#mid-wrap').delegate(...) handler to show it again milliseconds later.
Replacing
$('#mid-wrap').delegate('ul#lists', 'hover', funcIn, funcOut);
with
$('#mid-wrap').delegate('ul#lists', 'mouseover', funcIn).
delegate('ul#lists', 'mouseout', funcOut);
seems to do the trick.

Related

How to prevent duplicate event listeners upon appendChild?

I've been trying to create a scene that allows a user to switch between cameras upon clicking a "swap button" entity. I created a smaller CodePen demo to present the setup. Clicking on the swap entity (the sphere, here) will move the cursor between the old and the newly activated camera entities. When it does this, it has to call the init handler again. However, I'm having difficulty preventing this from causing an dogpile of event listeners on the cursor.
For the demo, the print-onenter component logs whether the cursor's over the a-sphere/box/cylinder/plane. Yet every additional camera swap by clicking on the sphere results in an additional print-out between mouseenter events (and you have to click it twice after, presumably because the clicks to swap will be undone by the additional click--but in the process add a different even/odd amount of listeners enabling you to actually swap next [set of] click event). I tried to apply a .removeEventListener, and to move print-onenter's function handler into a global scope so .removeEventListener would see it (since I assume anon handlers are effectively treated as unique types and therefore not treated as a duplicate listener and ignored).
function listenerTest (event) {
console.log("Object Entered: " + event.detail.intersectedEl.nodeName);
};
AFRAME.registerComponent( 'print-onenter', {
init: function() {
console.log('Reinitialized print-onenter.');
this.el.addEventListener( 'mouseenter', listenerTest );
},
remove: function() {
console.log('Deinitialized print-onenter.');
this.el.removeEventListener( 'mouseenter', listenerTest );
}
} );
Nevertheless, the same problem persists. Any help would be greatly appreciated.
Please see ngokevin's suggestion in the comments to the question above for a better way to handle this without reparenting entities.
Workaround: You could have two cursor enitites, one that has the cursor and one that doesn't. When you flip, then removeAttribute one cursor on one entity, and setAttribute another cursor on the other.

Disable Double Tap To Click On Touchscreen iOS Devices

So, quite recently I've been working on a website that was given me to improve and make responsive, and one of the issues that I've been faced with is that there are many elements that are clickable, with a mixture of CSS and jQuery effects for hover states.
Now, firstly I'd prefer all of these hover states to be CSS, but the main issue I'm having is that on these hover states, certain elements are changing display and visibility css properties. I did some reading, and apparently if this is the case, on touchscreen iOS devices, this causes the first 'touch' to force the hover state, and then a second click is needed to actually click the element.
I'm trying to find a solution that doesn't require lots of markup and styling changes. Preferably a fix harnessing jQuery/JavaScript would be good.
I've tried the following:
$(document).ready(function() {
$('a').on('click touchend', function(e) {
var el = $(this);
var link = el.attr('href');
window.location = link;
});
});
However, this has issues when the user holds their finger down on a clickable element, and drags the page to scroll. When they release their finger after dragging, the window.location is still changed.
I'll add a jsFiddle later if necessary.
Thanks in advance.
EDIT:
Here's a jsFiddle that shows the issue. http://jsfiddle.net/0bj3uxap/4/
If you tap one of the blocks, you'll see it shows the hover state, you then need to tap again to actually fire the click event.
It seems to happen when an element is hidden, and then the hover state shows the element.
Looks like I found a solution.
https://github.com/ftlabs/fastclick
FastClick fixes this issue, and removes the 300ms delay issue with some mobile browsers.
Just include the library in <script> tags, and then initiate it using jQuery, or whatever you prefer:
$(document).ready(function() {
FastClick.attach(document.body);
});
Just to explain briefly why the issue occurs:
When an element is hidden, for example when it has a CSS property of any of the following:
display: none;
opacity: 0;
visibility: hidden;
And the hover state of the hidden element then shows the element, iOS doesn't fire a click event on the first touch, it forces the hover state (to show the element). The user then needs to touch the element again for a click event to fire.
I see why this has been added, but I think I'd rather iOS didn't do this, and then developers would just need to tailor their websites to not hide content that coud be vital.
If it helps anyone else: In my case I had a very similar problem, however it wasn't simply due to a :hover style on it's own. Instead, it was due to the fact that I was using JavaScript event listeners (touchstart, touchmove and touchend) to change visibility of elements on the page (no matter where).
In my case, I was simply adding a touch class to the <html> tag in order to detect that the device was capable of touch and should always display certain elements that typically only show on hover. My fix was two fold:
Move to a >300ms delay (i.e. the amount of time mobile browsers may typically wait before determining if this was a single vs. double click). In my case, I just settled on 500ms (see #2 below for why).
I then used a cookie to temporarily retain this setting so that these elements would be visible immediately and no touch event listeners would be required on subsequent page loads (thus a delay of 500ms on the first occasion shouldn't be a deal breaker).
Example code:
In this case, using jQuery + https://github.com/carhartl/jquery-cookie (modified to support maxAge).
function initTouchSupport() {
// See if touch was already detected and, if so, immediately toggle the touch classes.
if ($.cookie('touch-device')) {
toggleTouch();
return;
}
// Be efficient and listen once and, if ever detected, flag capability and stop listening (for efficiency).
var events = 'touchstart touchmove touchend';
$body.on(events, detectTouch);
function detectTouch() {
// Detected; retain for a short while (e.g. in case this is a laptop with touch capability and they switch
// to mouse control). That way there's no delay on the next several page loads and no chance of a double-touch bug.
$body.off(events, detectTouch);
$.cookie('touch-device', true, {
path: '/',
domain: getDomain(),
maxAge: 86400 // 86400 seconds = 1 day
});
setTimeout(toggleTouch, 500);
}
function toggleTouch() {
// Swap out classes now
$html.toggleClass('no-touch', false);
$html.toggleClass('touch', true);
}
}
I had a very similar issue in IOS having to double tab buttons etc I removed all the desktop styles which included some hover styles this made no difference. I put the hover styles back in which are not used in the mobile UI. In then end the issue was a css class called
.error-message
Correction it turns out this css has been used in our UI and it was linked to a mouseover event

Google Maps API event listener priority

I have a map which has a google.maps.event.addListener set on the map object for click events to place a marker on the map. Now I want to add a context menu to the markers, so am creating a custom overlay in the floatPane. Again google.maps.event.addListener is used to add a rightclick event to each marker to position and display the menu.
Once the menu is displayed I want it to be cleared by either a menu item being selected, escape being pressed, or a click on the map.
The menu has a div for each items using jQuery .on to attach a click handler to them, whilst when the menu is displayed a keydown handler is attached to the document using .on to check for escape being pressed. These work as desired but I am unable to find a satisfactory solution to detecting a click on the map.
If I use google.maps.event.addListener on the map object to cancel the menu it works, but also registers with the listener to add a marker and event.stopPropagation() does not affect this. i.e. Cancelling the menu also adds a marker. I believe the Google API triggers the handlers in the order they were added with no way to change the priority.
I have also tried using a jQuery .on handler on the div to which the map is attached. But if I use click or mouseup event it is triggered by the right click which adds the menu. i.e. The menu flashes on screen as it appears but immediately closes. A mousedown event avoids this problem, but obviously still triggers the Google API handler to add a marker. It also registers the click to drag the map to scroll it, which the Google API does not and would be the preferred behaviour.
So it seems to me there are only two solutions to this problem.
One would be to cancel any handlers on the map when the menu is displayed, then re-add them once it closes. This seems unnecessarily excessive though.
The other is to make wrap the content of the handlers in a conditional statement to detect whether the menu is open. This could be either by using a global variable as a flag or adding a status property to the menu's object.
But this would make the code less reusable and go against the point of having separate event handlers. I may as well just put the code to close the menu in the original handler too.
Is there anything I am missing? It does not seem to be too obscure a thing to want to do but the inability to set the priority of handlers in the Google API means either having to code the handlers around each other.
For what it's worth, I believe your first approach (adding and removing handlers) is the "cleanest" (or most elegant?) approach.
If you think of the handlers as only having use/meaning when the menu is present, from a conceptual perspective, there's no need for them to exist when the menu is not visible.
If you bundle the event-wiring into the same function(s) or method(s) that handle the menu, then it becomes a self-contained, re-usable element that doesn't leave any "garbage" behind.
Since I don't have your code in front of you, here's some pseudocode:
function displayMenu() {
$menu.show()
.on('click', function() { /* etc */ });
}
function hideMenu() {
$menu.hide()
.off() // remove ALL event handlers
}
Or, even better, encapsulate it in an object:
var menu = {
show: function() {
// ...
}
hide: function() {
// ...
}
};
Or you could use a jQuery function too (though I generally eschew jQuery when I'm working with a map API, just to keep the number of dependencies low).

How to unbind() .hover() but not .click()?

I'm creating a site using Bootstrap 3, and also using a script that makes the dropdown-menu appear on hover using the .hover() function. I'm trying to prevent this on small devices by using enquire.js. I'm trying to unbind the .hover() event on the element using this code:
$('.dropdown').unbind('mouseenter mouseleave');
This unbinds the .hover of that script but apparently it also removes the .click() event(or whatever bootstrap uses), and now when I hover or click on the element, nothing happens.
So I just want to how I can remove the .hover() on that element, that is originating from that script, but not change anything else.
Would really appreciate any help.
Thanks!
Edit: Here is how I'm calling the handlers for the hover functions:
$('.dropdown').hover(handlerIn, handlerOut);
function handlerIn(){
// mouseenter code
}
function hideMenu() {
// mouseleave code
}
I'm trying to unbind them with this code.
$('.dropdown').unbind('mouseenter', showMenu);
$('.dropdown').unbind('mouseleave', hideMenu);
But its not working.
Please help!
**Edit2: ** Based on the answer of Tieson T.:
function dropdownOnHover(){
if (window.matchMedia("(min-width: 800px)").matches) {
/* the view port is at least 800 pixels wide */
$('.dropdown').hover(handlerIn, handlerOut);
function handlerIn(){
// mouseenter code
}
function hideMenu() {
// mouseleave code
}
}
}
$(window).load(function() {
dropdownOnHover();
});
$(window).resize(function() {
dropdownOnHover();
});
The code that Tieson T. provided worked the best; however, when I resize the window, until I reach the breakpoint from any direction, the effect doesn't change. That is, if the window is loaded above 800px, the hover effect will be there, but if I make the window smaller it still remains. I tried to invoke the functions with window.load and window.resize but it is still the same.
Edit 3: I'm actually trying to create Bootstrap dropdown on hover instead of click. Here is the updated jsFiddle: http://jsfiddle.net/CR2Lw/2/
Please note: In the jsFiddle example, I could use css :hover property and set the dropdow-menu to display:block. But because the way I need to style the dropdown, there needs to be some space between the link and the dropdown (it is a must), and so I have to find a javascript solution. or a very tricky css solution, in which the there is abot 50px space between the link and the dropdown, when when the user has hovered over the link and the dropdown has appeared, the dropdown shouldn't disappear when the user tries to reach it. Hope it makes sense and thanks.
Edit 4 - First possible solution: http://jsfiddle.net/g9JJk/6/
Might be easier to selectively apply the hover, rather than try to remove it later. You can use window.matchMedia and only apply your script if the browser has a screen size that implies a desktop browser (or a largish tablet):
if (window.matchMedia("(min-width: 800px)").matches) {
/* the view port is at least 800 pixels wide */
$('.dropdown').on({
mouseenter: function () {
//stuff to do on mouse enter
},
mouseleave: function () {
//stuff to do on mouse leave
}
});
}
else{
$('.dropdown').off('mouseenter, mouseleave');
}
Since it's not 100% supported, you'd want to add a polyfill for those browsers without native support: https://github.com/paulirish/matchMedia.js/
If you're using Moderizr, that polyfill is included in that library already, so you're good-to-go.
I still don't understand how you intend to "dismiss" the dropdown-menu once it is displayed upon mousing over the dropdown element partly because there's not enough code in your question, but that's sort of irrelevant to this answer.
I think a much easier way to approach the mousenter event handling portion is not by using off()/on() to unbind/bind events at a specific breakpoints, but rather to do just do a simple check when the event is triggered. In other words, something like this:
$('.dropdown').on('mouseenter', function() {
if($('.navbar-toggle').css('display') == 'none') {
$(this).children('.dropdown-menu').show();
};
});
$('.dropdown-menu').on('click', function() {
$(this).hide();
});
Here's a working fiddle: http://jsfiddle.net/jme11/g9JJk/
Basically, in the mouseenter event I'm checking if the menu toggle is displayed, but you can check window.width() at that point instead if you prefer. In my mind, the toggle element's display value is easier to follow and it also ensures that if you change your media query breakpoints for the "collapsed" menu, the code will remain in sync without having to update the hardcoded values (e.g. 768px).
The on click to dismiss the menu doesn't need a check, as it has no detrimental effects that I can see when triggered on the "collapsed" menu dropdown.
I still don't like this from a UX perspective. I would much rather have to click to open a menu than click to close a menu that's being opened on a hover event, but maybe you have some magic plan for some other way of triggering the hide method. Maybe you are planning to register a mousemove event that checks if the mouse is anywhere within the bounds of the .dropdown + 50px + .dropdown-menu or something like that... I would really like to know how you intend to do this (curiosity is sort of killing me). Maybe you can update your code to show the final result.
EDIT: Thanks for posting your solution!

Am I binding events over and over again in this jQuery code?

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.

Categories

Resources