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).
Related
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.
Working in HTML, CSS and JS (technically Angular, if you think it's significant)
I have a Header menu with dropdown sub-menus and sub-sub-menus when viewed in desktop style.
On a PC the sub-menus appear on hover and clicking on the entry takes you somewhere.
Clicking on the root entry takes you somewhere different - so it has 2 purposes in life: be a link to a location AND be the hover trigger for the dropdown menu.
Not all root elements have a sub-menu.
There's a separate mobile menu, based on width media queries, but Tablets (especially in landscape mode) display in desktop style, and it's giving me gyp!
On tablets (tested in Safari iOS and in Chrome for iPad) the browser does some deep magic!...
For elements that have No dropdown then clicking on them takes you to
the link.
For elements that DO have a dropdown sub-menu, then
the first click has the effect of activating the hover - it doesn't activate the link but does reveal the menu.
The second click then activates the link.
Lovely!
Problem is... the menus don't disappear if you tap off them.
Tapping the page in general does nothing (no click-event raised)
Tapping something on the page that is clickable DOES clear the menu, and also invokes the click action for whatever you clicked.
My first thought was to put a transparent div across the whole rest of the page, above the page but below the menu, and bind a click event that would clear the menu.
But, I can't get the transparent layer AND the underlying page to both be clicked. See here for the problems I hit there.
Putting the click event on body (so that it gets triggered by bubble up) doesn't work (click event just isn't fired.)
I tried adding ngTouch, and that does cause click events to be triggered everywhere, but also breaks the behaviour of the sub-menus opening - all the links trigger immeidately and you can't reach the sub-menus.
Any thoughts at all? Help!
You just need to check if click happened outside of dropdown menu.
Something like:
$('html').click(function (e) {
if (e.target.id == '#yourDropdown') {
//do nothing and let the click be handled
} else {
$('#yourDropdown').hide();
// still let the click be propagated
}
});
Rather then relying on 'mobile browser magic' I would implement it myself. No Angular experience, but in jQuery the code would probably look something like this:
$('.menu li').on('click', function(e) {
var $target = $(this);
var $sub = $target.children('ul');
// only if sub and not yet active
if ($sub.length && ! $target.hasClass('active')) {
// show sub
$target.addClass('active');
// done
e.stopPropagation();
return false;
}
// normal click action triggered here
alert($target.children('a').text());
e.stopPropagation();
});
$('html').on('click', function(e) {
// clear all active menu items
$('.menu > li.active').removeClass('active');
});
And a quick example: http://jsfiddle.net/b0aqr1wc/
:Sigh:
A colleague I discussed this with this morning solved this instantly:
Use the "cover sheet" approach, but have the sheet collapse itself at the same time as collapse the menu.
You lose the ability to interact with the underlying page for only 1 tap - and that tap will visibly collapse the menu, so the user will have a prompt to try again.
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();
}
}
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.
I find myself very often in the situation that I open an element in a web page - e.g. a drop-down menu - that I want to close if the user clicks anywhere on the page except the element itself.
To keep things simple, I have mostly written the code myself instead of employing some drop-down menu class.
However, I have never managed to build an implementation of this that was completely satisfying: Event handling and bubbling would work differently in different browsers, there would be the need for nasty workarounds, in some situations clicking the drop-down button would start closing it in the same moment, and so on.
Is there a Prototype based, authoritative, best practice to do this? Something that works across browsers - IE6 being a plus but not a requirement?
Just this:
click on a button - an element opens
(e.g. an absolutely positioned drop-down menu).
click within the element - the element stays open.
click on the button that opened the element - the element stays open.
click anywhere else on the page - the element closes.
I need help with the event handling part only, the displaying of the menu is totally secondary.
Event.observe(document, 'click', function (event) {
switch (event.element().id) {
case 'example_id':
// do different stuff depending on element clicked
// ofc u don't need to pass id, u can simply throw an element itself
break;
default:
// do close action
break;
}
// also check Event.findElement();
});
You can also add specific classes to the items you don't want to trigger close action and check it inside
if (!event.element().hasClassName('dont_close'))
Element.remove(selectDOMElement);
I guess the open button is within the menu.
$('openbutton').observe('click' function(event) {
var menu = $('openbutton').up();
if (menu.hasClassName('collapsed')) {
menu.removeClassName('collapsed');
menu.addClassName('expanded');
document.observe('click', function (event) {
if(!event.target.descendantOf(menu)) {
menu.addClassName('collapsed');
menu.removeClassName('expanded');
}
});
} else {
menu.addClassName('collapsed');
menu.removeClassName('expanded');
}
});
AFAIK, you need to make an invisible div the size of window, put it behind the current element, and add a click event to that.
Just thinking out loud but you might be able to use the blur event on the dropdown to hide it (blur gets fired when an element loses focus) or another idea might be when the dropdown opens attach a click event to the document object that hides the dropdown. Events get propagated through their containers so it should end up at the document object eventually. You might need to call preventPropegation on the event when your dropdown gets clicked so that it doesn't make it to the handler attached to the document.
maybe you could calculate the Position (X,Y) for the clickevent and compare that to the cumulativeOffset (cumulativeScrollOffset) + [el.width|el.height] of the desired container.
Event.observe(window, 'click', function(e) {
var el = $('el')
if( el.cumulativeOffset[0] < e.Event.pointerX(e) ... )
});
<div id="el" style="position:absolute;width:100px;height:100px;background-color:#00F;top:100px;left:300px;">
</div>