onclick() and onblur() ordering issue - javascript

I have an input field that brings up a custom drop-down menu. I would like the following functionality:
When the user clicks anywhere outside the input field, the menu should be removed.
If, more specifically, the user clicks on a div inside the menu, the menu should be removed, and special processing should occur based on which div was clicked.
Here is my implementation:
The input field has an onblur() event which deletes the menu (by setting its parent's innerHTML to an empty string) whenever the user clicks outside the input field. The divs inside the menu also have onclick() events which execute the special processing.
The problem is that the onclick() events never fire when the menu is clicked, because the input field's onblur() fires first and deletes the menu, including the onclick()s!
I solved the problem by splitting the menu divs' onclick() into onmousedown() and onmouseup() events and setting a global flag on mouse down which is cleared on mouse up, similar to what was suggested in this answer. Because onmousedown() fires before onblur(), the flag will be set in onblur() if one of the menu divs was clicked, but not if somewhere else on the screen was. If the menu was clicked, I immediately return from onblur() without deleting the menu, then wait for the onclick() to fire, at which point I can safely delete the menu.
Is there a more elegant solution?
The code looks something like this:
<div class="menu" onmousedown="setFlag()" onmouseup="doProcessing()">...</div>
<input id="input" onblur="removeMenu()" ... />
var mouseflag;
function setFlag() {
mouseflag = true;
}
function removeMenu() {
if (!mouseflag) {
document.getElementById('menu').innerHTML = '';
}
}
function doProcessing(id, name) {
mouseflag = false;
...
}

I was having the exact same issue as you, my UI is designed exactly as you describe. I solved the problem by simply replacing the onClick for the menu items with an onMouseDown. I did nothing else; no onMouseUp, no flags. This resolved the problem by letting the browser automatically re-order based on the priority of these event handlers, without any additional work from me.
Is there any reason why this wouldn't have also worked for you?

onClick should not be replaced with onMouseDown.
While this approach somewhat works, the two are fundamentally different events that have different expectations in the eyes of the user. Using onMouseDown instead of onClick will ruin the predictability of your software in this case. Thus, the two events are noninterchangeable.
To illustrate: when accidentally clicking on a button, users expect to be able to hold down the mouse click, drag the cursor outside of the element, and release the mouse button, ultimately resulting in no action. onClick does this. onMouseDown doesn't allow the user to hold the mouse down, and instead will immediately trigger an action, without any recourse for the user. onClick is the standard by which we expect to trigger actions on a computer.
In this situation, call event.preventDefault() on the onMouseDown event. onMouseDown will cause a blur event by default, and will not do so when preventDefault is called. Then, onClick will have a chance to be called. A blur event will still happen, only after onClick.
After all, the onClick event is a combination of onMouseDown and onMouseUp, if and only if they both occur within the same element.

Replace on onmousedown with onfocus. So this event will be triggered when the focus is inside the textbox.
Replace on onmouseup with onblur. The moment you take out your focus out of textbox, onblur will execute.
I guess this is what you might need.
UPDATE:
when you execute your function onfocus-->remove the classes that you will apply in onblur and add the classes that you want to be executed onfocus
and
when you execute your function onblur-->remove the classes that you will apply in onfocus
and add the classes that you want to be executed onblur
I don't see any need of flag variables.
UPDATE 2:
You can use the events onmouseout and onmouseover
onmouseover-Detects when the cursor is over it.
onmouseout-Detects when the cursor leaves.

onFocus / onBlur are events that don't bubble. There are however focus events that do bubble. These being focusin and focusout.
Now to the solution: We wrap both the input and our dropdown into a div-element and set the tabindex of that div to -1 (so that it can recieve focus / but does not appear in the tab order). We now add an eventlistener for focusin and focusout to this div. And since these events do bubble a click on our input element will trigger our divs focusin event (which opens the drop-down)
The neat part now is that a click on our dropdown will also trigger the focusin event on our div (so we basically maintain focus which means: focusout/blur never fires and our dropdown stays open)
You can try this out with the code snippit below (the dropdown only closes on loss of focus - but if you want it to close when clicking on the dropdown aswell just uncomment the one line of JS)
const container = document.getElementById("container")
const dropDown = document.getElementById("drop-down")
container.addEventListener("focusin", (event) => {
dropDown.classList.toggle("hidden", false)
})
container.addEventListener("focusout", (event) => {
dropDown.classList.toggle("hidden", true)
})
dropDown.addEventListener("click", (event) => {
console.log("I - the drop down - have been clicked");
//dropDown.classList.toggle("hidden", true);
});
.container {
width: fit-content;
}
.drop-down {
width: 100%;
height: 200px;
border: solid 1px black
}
.hidden {
display: none
}
<div class="container" id="container" tabindex="-1">
<input id="input" />
<div class="drop-down hidden" id="drop-down" > Hi I'm a drop down </div>
</div>
there arises however one issue if you want to add your dropdown into the tabbing order, have buttons in your dropdown or in general have an element in the dropdown, that can recieve focus. Because then a click will give the element in the dropdown focus first. This triggers our container div to lose focus which closes the dropdown so the focus event can't bubble further and therefore can't trigger the focusin on our container.
We can solve this issue by expanding the focusout eventlistener a bit.
The new eventlistener is as follows:
container.addEventListener("focusout", (event) => {
dropDown.classList.toggle("hidden", !container.matches(":hover"))
})
We basically say: "don't you close that dropDown if someone is hovering over it" (This solution only considers mouse-use; but in that case this is fine, because the problem this tries to fix only ever occured when using a mouse, when tabbing onto/through the dropDown everything worked fine from the start)

change onclick by onfocus
even if the onblur and onclick do not get along very well, but obviously onfocus and yes onblur. since even after the menu is closed the onfocus is still valid for the element clicked inside.
I did and it worked.

An ideal solution I found to work for me was to simply add a timeout in my onBlur function. I used 250ms, that provided smooth behaviour for my blur event and allowed my onClick to fire before the onBlur. I used this example as a reference https://erikmartinjordan.com/onblur-prevents-onclick-react

You can use a setInterval function inside your onBlur handler, like this:
<input id="input" onblur="removeMenu()" ... />
function removeMenu() {
setInterval(function(){
if (!mouseflag) {
document.getElementById('menu').innerHTML = '';
}
}, 0);
}
the setInterval function will remove your onBlur function out from the call stack, add because you set time to 0, this function will be called immediately after other event handler finished

Related

Autofocus not working when using onMouseDown

I have two almost identical components that make the focus for the input when it appears:
https://codesandbox.io/s/clever-knuth-7smon5?file=/src/index.js
https://codesandbox.io/s/wizardly-resonance-66lr8n?file=/src/index.js
In the second case, I use the onMouseDown event instead of onClick for the button that triggers the input. In both cases, the input appears, but in the second case, the focus does not work.
Yes, the reason why this is happening is because the click event is fired after the mousedown event therefore the focus is being moved almost instantly after to the button that was clicked.
I'm unsure of your exact use-case but one of the things you can do is use e.preventDefault() if you really want to use onMouseDown
<button
onMouseDown={(e) => {
setSearchIsOpen(true);
e.preventDefault();
}}
>
Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event

jQuery Click Event Triggering On Multiple Elements (I only want 1)

When my overlay comes up, everything works well, but I added some code to close out the overlay, but this code gets triggered even when I'm just clicking my arrows. The following is the code that's being triggered, which is fine when I'm not clicking the arrows to change the image. But when I click the arrows, the background which is the overlay is also being trigger, so the image is changing but the overlay is also hiding.
$('#overlay').click(function() {
$(this).fadeOut('slow');
});
How can I be able to use the arrows without it also clicking on the background overlay? If you open up the project, you will see what I'm saying.
To open the project:
https://htmlpreview.github.io/?https://github.com/rodriguesandrewb/photo_gallery_v1/blob/master/index.html
To open the repository:
https://github.com/rodriguesandrewb/photo_gallery_v1
You want to use event.stopPropagation(): https://api.jquery.com/event.stoppropagation/
This prevents the event from bubbling (being triggered by other elements)
Your outter most element is #overlay. It means that no matter where you click you'll be always clicking on your #overlay element. That is way your callback is being always triggered and closing your image.
To fix your problem and make your image close only when clicking on it you could use:
$('#changeImage').click(function() {
$(this).closest('#overlay').fadeOut('slow');
});
Ok, there's a ton of code to sort out, so I'm guessing your overlay is
<div id="overlay" style="display: block;"></div>
and your event.target is deep down inside this:
<div class="mainCenter">
<div class="container">
<div id="topFixed">
<input type="text" id="search" placeholder="Search">
</div>
<ul id="gallery">
.......
I'm not 100% sure where your event.target is, (the element you want to click and not everything else). But it's safe to assume that after you click your intended button, the event continues to bubble up the event chain. The event chain is basically your event.target's ancestors which includes#overlay` which is at the very top of the event chain.
To prevent event bubbling (btw bubbling is the default behavior but in instances such as your's it's not desired.) try placing stopPropagation() after or inside at the end of your event handler.
I wish I could be more specific as to where and how to apply this code as it pertains to your source, but you didn't provide the specific areas that concern your eventListeners, eventHandlers, etc...
The #overlay is used in this example but I suggest you use the event.target parent instead. The purpose of this code is to accept an event like 'click' on an element (i.e. button) or multiple elements (i.e. buttons) through their mutually shared parent. That's one place to click for potentially several different buttons. At first you'd think that's non-sense and you'd say, "Sure that button is clicked because the parent was clicked, but now everything the parent is chained to will trigger everything else."
That would be correct except we have stopPropagation(); at the very end of your eventHandler. That will stop propagation of the event bubbling back up the event chain, so there's no more rogue triggers lighting up everywhere. Rogue Triggers® sounds like a great band name. :P
For details and a much better explanation: http://www.kirupa.com/html5/handling_events_for_many_elements.htm
var overlay = document.querySelector("#overlay");
theParent.addEventListener("click", doSomething, false);
function doSomething(e) {
if (e.target !== e.currentTarget) {
var clickedItem = e.target.id;
alert("Hello " + clickedItem);
}
e.stopPropagation();
}

How to handle "click" event even if element is moved from beneath the cursor between mousedown and mouseup?

I have an <input> and a <button>.
The input has an onblur event handler that (sometimes) results in the <button> being moved; if, with focus on the <input>, the user goes to click on the <button> (and it is thereby moved from beneath the pointer before the click completes) they must currently move their pointer to the button's new location and click it a second time. This experience is suboptimal.
I would like for users to have to click the <button> only once (but must retain the functionality of the button moving when the <input> loses focus). However, if a user mousedowns over the button and then moves their mouse pointer away (includes switching focus from the application) before mouseup, no click event should be triggered (as normal).
I can't see any approach based on handling onmousedown and/or onmouseup that would not be prone to errors in some edge cases. All that I can think is to forcibly move the cursor in the onblur handler so that the click completes (if indeed it would?)—but this is probably a poor user experience too.
How can this best be handled?
$('button').click(
$('<li><input></li>')
.children('input')
.blur(function(){
if (!this.value.length)
this.parentElement.remove();
})
.end(),
function(e){
e.data.clone(true)
.appendTo($(this).data('target'))
.children('input')
.focus();
}
);
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
List 1<button data-target="#list1">+</button><ul id="list1"></ul>
List 2<button data-target="#list2">+</button><ul id="list2"></ul>
One approach would be to fade the element out over the course of, say, a second rather than immediately, which gives the user time to complete the click before the button moves:
$(this.parentElement).fadeOut("slow", function() {
$(this).remove();
});
Live example:
$('button').click(
$('<li><input></li>')
.children('input')
.blur(function(){
if (!this.value.length)
$(this.parentElement).fadeOut("slow", function() {
$(this).remove();
});
})
.end(),
function(e){
e.data.clone(true)
.appendTo($(this).data('target'))
.children('input')
.focus();
}
);
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
List 1<button data-target="#list1">+</button><ul id="list1"></ul>
List 2<button data-target="#list2">+</button><ul id="list2"></ul>
That said, I think I'd try to find a way that the space between the list elements didn't change at all, because this still means things move around, just at a different time — hopefully at a time the user will be less bothered by it, but... Tricky in this case, though, because of the lists being on top of each other — the second list is going to move at some point. If you made it that there was always an input box for the user to type in, then the second list would just move at a different time (when the box does have a value in it and you add a new blank one for the next value). Could be done with a fixed list size and scrolling within, but it depends on the overall design.
I had a related issue where I was styling buttons to shrink on being pressed, using the :active pseudo-class to resize the button on mouse-down and resetting its size when the button gained focus with the :focus pseudo-class. However, if the button was clicked close enough to its edge, it still was activated and shrank, but the cursor was now just outside of the bounds of the button and the button would no longer gain focus.
This is a result of how the click event itself works. For the onclick event to be fired on a given element, the cursor must be "both pressed and released while the [cursor] is located inside the element." However, if the cursor moves outside of the element before being released, the event is instead fired on the "most specific ancestor element" containing both elements. In specific, this results in a mousedown event being fired on the element and a mouseup event being fired on its ancestor element.
A solution, then, is to monitor a button's mousedown event, and click the button if its parent's mouseup event is fired (i.e., the cursor was pressed over a button but released over its parent).
Vanilla JS code:
// The CSS selector for the buttons to monitor
const selectors = "button";
window.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll(selectors).forEach(button => {
// Watch for button press
button.addEventListener('mousedown', event => {
// The button is the event's original target
const element = event.target;
// Click if the cursor is released over the button's parent
element.parentElement.addEventListener('mouseup', () => {
element.click();
}, { once: true });
});
});
});
This works best if the button's parent is sized the same as the button (such as with width: fit-content). Otherwise, unintentional clicks can occur if the cursor is released far away from the button, but still on the button's parent element.

How to hide widget on blur except when widget is clicked?

I'm building a widget akin to a datepicker, but I can't figure out how to make it disappear when when either (a) the user tabs out of the input box, or (b) clicks outside both the widget and input.
It's easy to bind a blur event to the input box, but the problem is that it will get triggered when you click on the widget, and there doesn't appear to be a reliable way to determine which element the focus was changed to from inside the blur event.
Closing the widget when the user clicks outside of the input is a bit sketchy too, but it's doable:
$('body').on('click', function(e) {
if(e.target != self.element[0] && e.target != self.clock[0] && !$.contains(self.clock[0], e.target)) {
self.clock.hide();
}
});
But I wouldn't need that if I could figure out how to handle the blur event properly (which may also be triggered by tabbing outside of the element).
Turns out the solution is actually quite simple. Thanks to rojoca's suggestion, I came up with this:
this.timepicker.on('mousedown', function(e) {
return false;
});
this.element.on('blur', function(e) {
self._parseInput();
self._refreshInput();
self._close();
});
The mousedown event fires first, and by returning false it prevents the blur event from triggering when clicking on the widget. Everything else (clicking outside the widget and input box, or tabbing away) causes a blur, which closes the widget as desired.
Further, it had the unintended by pleasant side-effect of keeping your cursor inside the textbox while interacting with the widget.

jQuery blur handler not working on div element?

I don't understand why the jQuery blur handler isn't working in the most simple case. I'm literally creating a div 100px by 100px and setting a blur event on it, but it's not firing (JSFiddle):
<div id="test">this is a test</div>
$(document).ready(function() {
$('#test').bind('blur', function() {
alert('blur event!');
});
});
Is my understanding of blur wrong? I expect the blur event to fire when I click anywhere that is not the div...right?
According to jQuery's documentation:
In recent browsers, the domain of the event has been extended to
include all element types. An element can lose focus via keyboard
commands, such as the Tab key, or by mouse clicks elsewhere on the
page.
I've tried it on the latest Chrome and Firefox on Mac.
From the W3C DOM Events specification:
focus
The focus event occurs when an element receives focus either via a pointing device or by
tabbing navigation. This event is valid for the following elements: LABEL, INPUT, SELECT,
TEXTAREA, and BUTTON.
blur
The blur event occurs when an element loses focus either via the pointing device or by
tabbing navigation. This event is valid for the following elements: LABEL, INPUT, SELECT,
TEXTAREA, and BUTTON.
The jQuery docs state browsers extended the events to other elements, which I'm guessing means blur and focus are aliases for the more generic DOMFocusIn and DOMFocusOut events. Non-input elements aren't eligible to receive those by default though, and an element has to somehow gain focus before losing it - a blur still won't fire for every click outside the div.
This SO question mentions that giving an element a tabindex would allow that, and seems to work for me in Chrome after modifying your jsFiddle. (Albeit with a fairly ugly outline.)
As far as I knew, blur happens on inputs that had the focus, either way you say
I expect the blur event to fire when I click anywhere that is not the div...right?
Not exactly, the blur event only happens for an element that had the focus first
So in order for a blur event to occur, you would first have to give focus to the div, how is the div getting focus first?
If you are really try to determine if there was a click outside of your div, you need to attach a click handler to the document, and then check to see where your click came from.
var div_id = "#my_div";
var outsideDivClick = function (event) {
var target = event.target || event.srcElement;
var box = jQuery(div_id);
do {
if (box[0] == target) {
// Click occured inside the box, do nothing.
return;
}
target = target.parentNode;
} while (target);
}
jQuery(document).click(outsideDivClick);
Just remember that this handler will be run for EVERY click on the page. (in the past if i ha to use something like this, i attach the handler when I need it, and remove it when I no longer need to look for it)
A can't "blur" because that would involve the div having focus in the first place. Non-input elements like a and textarea can have focus, which is what jQuery's documentation refers to.
What you need is the "mouseout" or "mouseleave" event (mouseleave doesn't bubble, mouseout does), which will be fired when the cursor leaves the div. If you need to have clicks, I would attach a "click" event to the body, as well as the div and stopping the event propagation on only the div:
$("div").click(function(e) {
return false; // stop propagation
});
Or, if you're really determined, you can fake the appearance of a div with a and some CSS rules :)
If you want something to happen while you move your mouse over the box, you could use the mouseover event.

Categories

Resources