We have custom context menus that replace the browser menus on right click. We would like to be able to abort (hide) the context menu in the same way that the default context menu is hidden by the browser- a click anywhere outside the menu that does not register as a click. The solution should apply to both bound events and default browser actions, but not impede the ability for other events, ie. hover from firing.
An example:
In firefox, right click on this page to open the context menu. Hover over the
Questions-Tags-Users-Badges-Unanswered
at the top of this page. Even though the context menu is open, highlighting still occurs. Now, click on a text area on this page, like the search box at the top. The context menu will hide, but your cursor will not focus the text box (unless you click it again with the menu closed).
How can we emulate this behaviour in JavaScript?
Rejected options we've considered:
Use a transparent div over the whole page. Problem: This can capture clicks anywhere, but breaks hover events and hover css.
Check for a context-menu-open variable in each click handler, and assign handlers to all links, and input elements to detect the context menu open state, which closes the context menu, unset the open state and prevents the handlers default. Problem: Very sloppy code and a maintenance nightmare.
Consider a variation of rejected option #2, where you have a single event listener on the document.
document.addEventListener('click', function (e) {
if (contextMenuOpen) {
e.preventDefault();
e.stopPropagation();
}
}, true);
For more information about that true, look up useCapture and event flow.
Guest was on the right track with event capture, but the solution had a few glitches. This is a more robust solution that solves the following problems:
Don't immediately close the menu in the right click event that fires right after context menu.
Don't let text fields get focused when the context menu is open- the focus event fires first and is not caught by a capture click event. We need to setup a capture handler on focus too.
Deal with the problems created by having a focus and click handler, which both fire.
To do this, you need two capture event handlers:
document.addEventListener('focus', function(e){eDocumentFocusCapture(e)}, true);
document.addEventListener('click', function(e){eDocumentClickCapture(e)}, true);
// If the context menu is open, ignore the focus event.
eDocumentFocusCapture = function(e){
if(RowContextMenuIsOpen){
console.log('focus event sees the menu open: cancel focus, but leave menu be. Click will take care of closing it.');
e.stopImmediatePropagation();
e.preventDefault();
e.target.blur(); // tell the clicked element it is not focused, otherwise you can't focus it until you click elsewhere first!
}
}
eDocumentClickCapture = function(e){
// A right click fires immediatly after context menu is fired,
// we prevent the menu from closing immediately by skipping one right click event
if(RowContextMenuWasJustOpened===true && e.button===2){
console.log('right click self bypass');
RowContextMenuWasJustOpened=false;
return;
}
if(this.protected.RowContextMenuIsOpen){
console.log('click event sees menu open, I will close it.');
this.HideRowContextMenu();
e.stopImmediatePropagation();
e.preventDefault();
}
}
Related
I am using coveo Framework and i used facets inside a dropdown button i wrote a window.onclick function so that when clicked outside dropdown button the dropdown should be closed.
everything seems to be working fine but when i clicked facets checkbox the dropdown was closing and when i talked to coveo team they said the query was triggered when coveo checkbox was clicked thats the reason the dropdown was closing when clicked.
To fix this i used event.stopPropogation and that was working fine in desktop mode but when it comes to IPAD Mode this is not working any help
Here is my code
// Prevent event bubble up to window.
document.getElementById('dropdown').addEventListener('click', function(event) {
event.stopImmediatePropagation();
});
function close() {
document.getElementById('dropdown').classList.remove('show');
}
window.onclick = function(event) {
if (!navigator.userAgent.match(/Android|webOS|iPhone|iPod|Blackberry/i)) {
if ((!event.target.matches('.dropdown-backdrop')) && event.target.closest('#dropdown') === null) {
close();
}
}
};
I believe the issue is on a touch screen device, you actually get touch events possibly in addition to mouse events. I suspect if you attached another listener to touchstart that does the same thing as click you will see the same results on the tablet.
In theory you should see no click events on a tablet (a user cannot click without a mouse) but in practice the browser emulates click events. However, when those events are generated the browser may fire both touch events and mouse events in response to the same user input. If both events are fired you're successfully stopping the click event from propagating but not the touch event.
Update
You haven't given enough detail to fully give an example, but the change happens in the listeners you attach to your dropdown element.
// Note instead of using the same anonymous function twice,
// I've defined a function to stop propigation
function stopProp(e) {e.stopImmediatePropagation();}
document.getElementById('dropdown').addEventListener('click',stopProp);
document.getElementById('dropdown').addEventListener('touchstart',stopProp);
So I have a form where double-clicking a field brings up a custom modal window. The buttons for "Save" and "Cancel" on the modal window have "click" events that call hide() on the modal window layer. However, some of our users naturally double-click things. Double-clicking the save or cancel buttons fires the click event and hides the modal window but also fires the double-click event of the field that was under the modal window causing the modal window to display again. I know using a setTimeOut() and delaying the hide() of the modal window will resolve the issue but I prefer not to degrade the responsiveness of the UI if possible. Any suggestions?
Here is a fiddle to generally explain the problem.
https://jsfiddle.net/e51rc24j/4/
$(function() {
$(".field").on("dblclick", function(ev) {
$(".hoverlayer").show();
});
$(".hoverlayer").on("click", function(ev) {
var thisLayer = this;
$(thisLayer).hide();
/* PUTTING IN DELAY ON HIDE SOLVES PROBLEM BUT I PREFER TO NOT DELAY UI RESPONSIVENESS IF POSSIBLE
setTimeout(function(){
$(thisLayer).hide();
}, 300);*/
});
});
At first I thought the problem was propagation, so I added a stopPropagation to your event. But then I found out that it's not the double click that's propagating. The problem is something completely different, namely that the SECOND click (of the double click to close the black overlay) LANDS on the input field again, which triggers the double click event on the input box again.
So all you have to do, is move the SAVE and CANCEL buttons so they are not directly on top of the input field.
I have made a little change to your jsfiddle to illustrate:
https://jsfiddle.net/e51rc24j/6/
If you double click in the "PROBLEM", the modal black div will open again, because your second click lands INSIDE the <input> text field.
However, if you double click anywhere else in the black div ("Not a problem"), it will not open.
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.
for arcane reasons I need to be able to cancel the click event via the mousedown event.
Briefly; I am creating a context menu in the mousedown event, however, when the user clicks on the page the context menu should disappear.
I am not able to use the mousedown event over the click in that scenario as I want the user to be able to click links inside the menu ( a full click would never travel to the <a> based menu elements ).
If it is any help, jQuery can be applied.
I would like to either be able to prevent the click event from happening from within the initial mousedown, or be able to pass information to the click event (via originalEvent or otherwise).
TIA
Seems to be impossible, neither FF nor Opera didnt cancel upcoming click when prevented in mousedown and/or mouseup (as side note: click is dispatched after mouseup if certain conditions met). testcase: http://jsfiddle.net/ksaeU/
I have just had the exact same problem. I fixed my context menu by closing it on mousedown and eating the mousedown event on the menu so that I can still receive clicks on the menu, like so:
$(document).one('mousedown.ct', null, function() { cmenu.hide(); return false; });
cmenu.bind('mousedown', function(e) { e.stopImmediatePropagation(); });
And in the hide() function I unbind the mousedown.ct again, in case it was closed due to a click on an item.
Hey, I think this is what you are trying to do with your code. If not, I apologize, I may have misunderstood the question. I used jQuery to get it done: http://jsfiddle.net/jackrugile/KArRD/
$('a').bind({
mousedown: function(){
// Do stuff
},
click: function(e){
e.preventDefault();
}
});
In the jQuery autocomplete/menu widget (the autocomplete widget is based on the menu widget, which is a still unreleased widget), how is a click outside of the menu detected ? (A click outisde of the menu closes the menu)
I have added a srollbar (similar to the classic select element) to that menu in a custom combobox widget I am writing. The problem is that in IE8, a mousedown on the scrollbar is detected as a click outside the menu, which closes it, making the scrollbar useless. So, to work around this issue, I am first trying to understand how the menu widget works.
You can view the source here, it's basically just checked when it's blur event fires, and hiding 150 seconds after, if the click wasn't in the menu portion:
.bind( "blur.autocomplete", function( event ) {
clearTimeout( self.searching );
// clicks on the menu (or a button to trigger a search) will cause a blur event
self.closing = setTimeout(function() {
self.close( event );
self._change( event );
}, 150 );
});
Other areas of autocomplete, e.g. the selection menu itself clear this timeout, preventing the hide...a blur caused by something else doesn't, resulting it in being hidden. It's worth noting this is not the way for you to replicate the behavior, there are better ways by preventing bubbling, but if your goal is to understand this widget specifically...well that's what it does :)
It would be easier to dismiss the menu not after a click but if the mouse leaves the menu (after a short grace-period).