Dispatched click event: Prevent browser to jump to clicked element - javascript

I have a quite complex product in WooCommerce with a complicated variation selector.
To simplify things you can just click on a photo of a variation you like (anywhere on the page) and have it selected for you.
So I just added an eventlistener on the photos that triggers a click on the correct variation checkbox, as soon as you click the image:
Photo_Variation1.addEventListener('click', () => {
Variation1.click()
})
This works fine, but then the browser automatically jumps to the original variation selector – because it gets focused after/during the click-event. This is rather confusing UX-wise.
So I tried for hours to find a solution. But so far my only lousy success was making it work in Safari, by adding this to the eventListener:
Variation1.blur()
I found out it doesn't work in other browsers, because there the focus event happens directly after the mousedown-event. (In Safari it's after the click.)
However, when I tried to solely trigger the mousedown function (with a "preventDefault") nothing gets selected, at all.
Do you have an idea how I can trigger a click and prevent focusing the clicked element or any another workaround that stops the browser from jumping around?
PS:
I am using JavaScript for just 8 hours, so have mercy, please. :D

You can programmatically set a checkbox as checked by doing the following:
Photo_Variation1.addEventListener('click', () => {
var myCheckBox = document.getElementById("myCheckBoxId");
if(myCheckBox.checked) {
myCheckBox.checked = false;
} else {
myCheckBox.checked = true;
}
})
Here is more info to help you as well.
https://www.w3schools.com/jsref/prop_checkbox_checked.asp

Related

Intercept event before it fires when addEventListener doesn't work

I've been maintaining a small project written in React. It's a browser extension which adds Slack-like emoji support to Facebook Messenger. Since Facebook is changing up thing once in a while, I need to make new changes to the browser extension, so they work just like before.
Facebook uses a <div> with the contenteditable attribute set to true. They have done this for as long as I remember. However, they changed something up, because I used to be able to do:
document.querySelector("div[contenteditable=true]").addEventListener(e => {
if(e.key == "Enter" && isActive) { //isActive means if the emoji picker is visible or not
e.preventDefault();
e.stopPropagation();
selectEmoji();
}
});
However, that event no longer fires, even though the element exists. It fires for EVERY key except Enter. So I started digging with a MutationObserver, and to my surprise, I noticed this:
removedNodes: NodeList[<div with contenteditable=true>]
addedNodes: NodeList[NEW <div with contenteditable=true>]
So once I hit enter, Facebook sends the message and then replaces the entire "chatbox" element (the div with contenteditable=true) with a new chatbox element.
What I don't understand, is how my addEventListener isn't firing, when I hit the Enter key. Even if I do something like this, it won't work:
setInterval(() => {
var chatbox = document.querySelector("div[contenteditable=true]");
chatbox.addEventListener("keydown", () => ........ });
}, 1000);
Even if Facebook is removing the element, shouldn't my event fire anyway? According to my test here, it should fire (at least the first time): https://jsfiddle.net/web4dyg1/
Since adding an event listener, and then cancel Facebook's request (previously form submission), doesn't work anymore, what other options do I have? I know this question is really niche, but I was hoping someone had an thinking-out-of-the-box idea.
EDIT: I need to clarify that adding the event listener works. Every single keypress fires the event EXCEPT for the Enter key.

Polymer 1.5/iOS: How to stop event propagation over iron-pages

We have a one page app which uses iron pages and express-router to navigate. In the browser and on android it works perfectly, on iOS however, we have a bug. The problem occurs if we switch pages by a button press. If the button is exactly over an input text field element (read: in the same position, but on the next iron-page) the input element gains focus directly after the page switch.
We used to have this problem as well with two buttons in the same position but this was solved by changing all on-clicks to on-taps.
Things we have tried so far:
Adding event.stopPropagation to the on-tap event
Including fastclick.js to prevent click delays (this worked partially when on-clicks were still in place but was made obsolete with on-tap)
Note that we have experienced this problem since Polymer 1.0 all through 1.5.
I reproduced your symptoms on an iPad Air 2, and trying e.stopPropagation(), e.preventDefault(), and returning false all had no effect. I'm not sure whether this is actually a Polymer problem.
I have a couple [hacky] workarounds to employ during the page-switch:
Option 1: Delay the page-change by 400ms. If your button has a ripple effect, the delay is masked by the animation.
codepen
Option 2: Disable the input and re-enable it after a 400ms delay. This prevents the input from picking up the tap event, but has the disadvantage that the disabled state could be noticeable (perhaps a lesser evil than your current problem).
codepen
Thanks #tony19, for the input.
We wanted to avoid delays, so I researched a bit more and ultimately fixed the problem. To answer my own question: the ultimate solution did lie in the FastClick library.
Basically what happens is that the tap event is fired immediately, but it doesn't replace the click event. Rather, the click event still fires, but with the original 300ms delay. This delayed click event thus fires on the newly displayed 'page' and triggers the input-field (or button had it been there) at the same x-y coordinates.
Adding the FastClick library once again solves this thanks to some updates in the library. However, it breaks some items that need the original click, such as Google Autocomplete. A basic solution to exclude FastClick is to instead apply it as:
FastClick.attach(document.body, {
excludeNode: 'something', });
This, however, only works for that node and not possible children. As such, to fix everything for input fields with Google's Autocomplete as well is done using:
// Disable FastClick for the children of a google auto-
// complete component.
var needsClick = FastClick.prototype.needsClick;
FastClick.prototype.needsClick = function(target) {
if ( (target.className || '').indexOf('pac-item') > -1 ) {
return true;
} else if ( (target.parentNode.className || '').indexOf('pac-item') > -1) {
return true;
} else {
return needsClick.apply(this, arguments);
}
};
// Remove click delay on iOS. As such, removing differences
// in timing between click and tap, thereby avoiding the
// fall-through problem.
FastClick.attach(document.body);
I will now close this thread, but I thought it'd be nice to leave this as reference for anyone else experiencing the problem.
Understand that it affected Polymer 1.0 to 1.5. Just to confirm that we experienced the same behaviour in Polymer 1.6 and the following fixes it.
_onTap: function(event) {
event.preventDefault();
event.stopPropagation();
}

Disable taphold default event, cross device

I'm struggling to disable default taphold browser event. Nothing that I have found on Google provided any help. I have only Android 4.4.4 mobile and Chrome dev tools for testing. I tried CSS fixes, such as webkit-touch-callout and others, but apparently they don't work for Android, also they don't work in Chrome dev tools.
I also tried detecting right click, (e.button==2), it doesn't work.
I came up with a solution, but it solves one problem and creates another. I just want to have a custom action for 'long press' event for selected anchors and I don't want the default pop up to appear (open in a new tab, copy link address, etc.)
This is what I did:
var timer;
var tap;
$("body").on("touchstart", my_selector, function(e) {
e.preventDefault();
timer = setTimeout(function() {
alert('taphold!');
tap=false;
},500);
});
$("body").on("touchend", my_selector, function() {
if(tap) alert('tap');
else tap=true;
clearTimeout(timer);
});
It successfully disables the default taphold event and context menu doesn't appear. However it also disables useful events, such as swipe. The links are in a vertical menu and the menu is higher than the screen, so a user has to scroll it. If he tries to scroll, starting on an anchor, it won't scroll, it will alert 'tap!'
Any ideas how could I disable taphold default or how could I fix this code so it disables only tap events and leave default swipe events enabled?
Edit: Now I thought about setting a timeout, if the pointer is in the same place for lets say 100ms, then prevent default action. However e.preventDefault(); doesn't work inside setTimeout callback.
So now I'm just asking about the simplest example. Can I prevent default actions after certain amount of time has passed (while the touch is still there).
And this is my whole problem in a fiddle. http://jsfiddle.net/56Szw/593/
This is not my code, I got this from http://www.gianlucaguarini.com/blog/detecting-the-tap-event-on-a-mobile-touch-device-using-javascript/
Notice that while swiping the box up and down, scrolling doesn't work.
I got the solution. It was so simple! I had no idea there's an oncontextmenu event. This solves everything:
$("body").on("contextmenu", my_selector, function() { return false; });
For an <img> I had to use event.preventDefault() instead of return false.
document.querySelector('img').addEventListener('contextmenu', (event) => {
event.preventDefault();
}

Dropdown plugin closing on scroll bar click

I'm in the process of teaching myself how to write a jQuery plugin. I am using the jquery-hover-dropdown-box as a base example. It's not just copy/paste though, I've made a number of changes trying to get a better understanding of it all. For example I'm not incorporating the hover event, I added a filter, and currently not using any defaults to name a few. Clicking on a div's scroll bar fires the blur event in I.E is the only post I've found with what looks like a good resolution to this and I tried implementing something similar but was unsuccessful.
Complete Example: jsFiddle
Issue:
I click in the input and the dropdown opens but the first time I click on the scroll bar, the dropdown closes. When I open the dropdown a second time and click on the scroll bar, it does not close (as I would expect). From what I can tell, my issue is in the blur on the input. I understand that when I click in the scroll bar, the input has lost focus. I tried to implement something similar to this post on Scrollbars not working on dropdown in IE8 but was unable to get it working.
Steps to Reproduce:
Click in the input to open the dropdown
Click anywhere in the scroll bar and the dropdown closes (should stay open and scroll)
Click in the input a second time and the dropdown opens
Click anywhere in the scroll bar and the dropdown stays open (as it should)
Question:
What am I doing wrong that is causing the dropdown to close only the first time I click on the scroll bar?
What I've Tried:
When I'm appending the ul to the div (currently commented out around line 68 in the jsFiddle), I added the code below. I figured that if I stopped the action from being triggered with a mousedown on the ul it would fix my issue. Although it did fix the issue in Chrome, it persists in IE8.
Update: I changed the code below from $list.mousedown... to $container.mousedown... since $list is the ul and $container is the div that contains it. My thought was that it extend the area. The result was the same though.
...
$container.append($list);
$list.mousedown(function(e) {
e.preventDefault();
});
...
Since this seemed to be close, I tried taking a similar approach in the blur event. The issue explained above happens when I use this code. In Chrome, clicking the scroll bar does not fire the blur event but in IE8, it does. The first time the dropdown is opened and you click in the scroll bar, it logs "hiding". Open the dropdown again and click the scroll bar and it logs "bind mousedown". Click anywhere outside the dropdown and it closes (as it should) and logs "hiding" (as it should). To me it seems backwards, but obviously I'm not understanding it correctly. (The code below is around line 134 in the jsFiddle)
Code edit: Updated with Goran.it suggestion to prevent multiple bindings from happening.
...
// where $dom is the 'div' containing the 'ul'
$dom.unbind('mousedown.auto_dropdown_box_ul')
.bind('mousedown.auto_dropdown_box_ul', function(e) {
console.log('bind mousedown');
e.preventDefault();
});
setTimeout(function() {
console.log('hiding');
$dom.addClass('auto_dropdown_hide').hide();
}, 100);
...
I've also tried removing the blur event. I know this would prevent the dropdown from closing if you tabbed out of the input but figured it was worth a try. In Chrome it works exactly how I expected, clicking outside the input closes the dropdown, clicking the scroll bar does not close it and tabbing out does not close it. In IE8, clicking outside the dropdown does not close it though, nor does it close when you tab out, but clicking in the scroll bar does work. This is the code I added after removing blur (it's not included in the jsFiddle).
// below where the 'blur' event was
$(document).click(function(e) {
if (e.target == dropdownArray[0].input[0] || e.target == dropdownArray[0].dom[0]) {
console.log('matches');
e.preventDefault();
} else {
console.log('does not match');
dropdownArray[0].dom.addClass('auto_dropdown_box_hide').hide();
}
});
Again, this is my first attempt, I'm still learning. I'm sure there are multiple things that I'm probably doing wrong, that I can improve, etc. Before I tackle those, I would just like to understand what I'm doing wrong here and what I need to do to correct it. After reading the plugin concepts, I know there is much for me to learn.
I found few issues on a first look, you should change the :
$dom.bind('mousedown.auto_dropdown_box_ul'
to:
$dom.unbind('mousedown.auto_dropdown_box_ul').bind('mousedown.auto_dropdown_box_ul'
To prevent multiple events binding to the dom node, you can also use .one event handling of jQuery.
In the same event handling you should also put:
console.log('bind mousedown');
e.preventDefault();
return false;
To be sure event is not firing.
Hope this helps (I'm not having IE8 for a long time now)
I believe I finally figured this one out. After multiple tries I thought I'd change up the format to one that seemed, at least to me, a little more straight forward.
Here is the complete jsFiddle
The underlying fix was correctly setting/adjusting which element has focus and when. Since mousedown executes before click, I stuck with that event on the dropdown. In the mousedown event, I set isVisible = true and set focus back on the input (although the latter is not completely necessary). In the blur event, I'm checking isVisible. If it's true, that means that a click happened in the scroll bar so don't close the dropdown. If it's false, close the dropdown. Throughout events, I'm keeping track of isVisible so I know it's state when blur executes. Again, I changed up the format so the two fiddles do look different. I'm sure I could go back and implement something similar to the original fiddle and get it working but I just liked this way more. Here is a snippet of the relevant changes:
{
// some code above
// where $list is the 'ul'
$list.bind('mousedown', methods.onDropdownMousedown);
// where $obj is the 'input'
$obj.bind('blur', methods.doOnBlur);
},
onDropdownMousedown: function(e) {
$input.focus(); // not really needed, just in case
isVisible = true;
},
doOnBlur: function(e) {
if (isVisible) {
$input.focus();
isVisible = false;
} else {
// where $container is the 'div' containing the list
$container.addClass('auto_dropdown_box_hide').hide();
isVisible = false;
}
isVisible = false;
}

jQuery find(':focus') not acting as expected

I'm making a widget that slides in and out of view on hover with showTracker and hideTracker functions. I want to prevent it from sliding out of view if it contains a focussed form element though, so I've got this going:
function hideTracker(){
if($('#tracker').find(':focus').length == 0){
$('#tracker').stop().hide();
}
}
Cool. Now it doesn't hide if the mouse happens to move out if there's a field in focus. Unfortunately, that also means that when the field does lose focus (and it's time for the widget to hide again) it just stays there. The unHover event has been and gone.
So I added this:
$('#tracker *').blur(function(){
hideTracker();
});
And that works too - with one little bug that I need help with!
If the focus moves from one element within the tracker to another which is also within #tracker, the tracker hides. I figured that if($('#tracker').find(':focus').length == 0) would return false, given that the next form element has focus, but I guess it doesn't.
Is it the case that .blur() fires before the next element attains focus?
How can I get around this?
How about something like this?
$('body *').focus(function(){
if(!$(this).is('#tracker *') && $('#tracker:visible').length != 0) hideTracker();
});
Yikes. Tricky. Yes, what's happening is:
mousedown: old form element gets the blur event. $(':focus').length == 0.
mouseup: new form element gets the focus event. $newFormElement.is(':focus') == true.
This is an improvement:
$('#tracker').focusout(function() //basically like $('#tracker, #tracker *').blur(), but "this" is always '#tracker'
{
if(!$(this).is('#tracker:hover')) //for some reason plain old :hover doesn't work, at least on the latest OS X Chrome
hideTracker();
});
But it's not perfect. It only really works if you use the mouse. If you use tab to move between fields (or some other possible mechanism) while your mouse is not hovering over #tracker, it won't work.
Here's another attempt. It's a bit...hackier. The gist is that, instead of handling the blur event, you handle the focus event of the second thing that's focused. But! What if you click something that can't be focused? Blank space on your page? Then no focus event is fired.
Okay. So the trick is: put a tabindex="0" in your root <html> tag. This means that there is always something that can be focused. So there's no way to focus on nothing (at least, I don't think so).
Then you can do this:
$('*').live('focus', function(e)
{
if(!$.contains($('#tracker')[0], this)) //if the new thing you focused on is not a descendant of #tracker
hideTracker();
e.stopPropagation();
});
Eh? So yeah, that's a certified hack. But it's a tough problem, and that's the best I can come up with at this hour.
Thank you all for your answers. Utilising the .focus() event rather than .blur() was a clever way to look at it. Unfortunately, it does raise a couple of browser problems, and I couldn't get any of the above working very robustly.
In the end I decided to use setTimeout(hideTracker, 100); to allow the focus() event to take place before the count of focussed elements within tracker was evaluated. Not ideal, but it's working well and the delay is fairly imperceptible.
Thanks again.

Categories