1

Suppose you have the following structure in your HTML and some elements inside the content div have various key (press, up, down) events:

<html>
<head></head>
<body>
    <div id="content">All the content</div>
</body>
</html>

You add an absolute positioned div by jQuery:

$('body').append('<div id="lightbox">etc.</div>');

How can you temporarily (during the existance of the lightbox) prevent all keyboard events from being caught by or bubbled to only $('#content') and its children, so that the bubbling order when the lightbox exists for the above structure will be

  1. $('#lightbox *')
  2. $('#lightbox')
  3. $('body')
  4. $('html')

whereas it will be

  1. $('#content *')
  2. $('#content')
  3. $('body')
  4. $('html')

when the lightbox is gone?

EDIT: Here's some extra info.

  • I can't make all the form elements disabled or readonly (this applies to limited elements anyway) because lightbox may occasionally exist on top of many form elements that have already assigned unique key events.

  • Also, I want key events to affect window + (lightbox, if any, otherwise content) so I can't just stop all event propagation directly

  • The orders above can be 4-1 instead of 1-4, it doesn't matter. What matters is to bypass the contents of this specific element (lightbox or content acc. to the situation) in key events.

inhan
  • 7,394
  • 2
  • 24
  • 35

3 Answers3

1

If I understand correctly, you need to selectively inhibit key events when the lightbox is showing.

I think the easiest way to do this will be to put the inhibitions inside the event handlers, as follows :

  • raise/lower a flag when the lighbox is active/inactive
  • inside all handlers for events you wish to be inhibited, return immediately if the flag is raised.

(You may be able to test the lightbox state directly, without needing to employ a flag)

I think this approach should be much easier than the alternatives which, broadly speaking involve either :

  • preventing/allowing event propagation
  • detaching and re-attaching handlers.

EDIT

Inhan, after reading your own solution, here's my thinking on keeping things simple.

Tabbing

There must be an easier approach to controlling tab-order than emulating it when a lightbox dialog is open.

If the entirety of the effect you need to achieve is the suppression of tabbing to form elements not in the currently open dialog (and assuming the jQuery UI lightbox) then simply set the modal: true option. For other lightbox plugins, see their documentation.

Bubbling

You should be able to eliminate unwanted bubbling of events from the lightbox to 'document' or any other container by appropriately contructing your HTML. Try following these rules :

  • Don't delegate any events to document. Choose more local containers. If necessary, wrap everything other than the lightbox(es) in a <div id="pseudoBody">...</div> for the express purpose of event delegation, ie. delegate events to this dedicatd, unstyled wrapper instead of document.
  • Ensure that your lightbox(es) are not nested inside inside containers to which events are delegated, eg. by placing lighbox(es) no deeper than one level within the <body> and certainly outside the pseudoBody if adopted.
  • If you need to allow some lightbox events to bubble, then delegate to the lightbox container(s). For economy of code (and if appropriate) you can attach the same (named) event handlers as are attached to the pseudoBody or elsewhere.
Beetroot-Beetroot
  • 18,022
  • 3
  • 37
  • 44
  • Exactly. My problem is, I have numerous unique focus/blur/key(down|up|press) events attached to elements so I'll write what I ended up with in a separate post. Is this something unusual that I'm asking by the way? – inhan Sep 16 '12 at 01:21
  • @inhan, no not unusual at all. Branching within event handlers is quite common. There must be a million reasons to do so. Can you post a link to your new post here please. – Beetroot-Beetroot Sep 16 '12 at 07:09
0

The only way to manipulate the native propagation order of events is to change the DOM order. You can't leave your DOM structure one way and designate a custom propagation order that is somehow different from the DOM structure. Events bubble up the DOM. That's how the browser does event propagation.

You can make the lightbox cover your entire page (position absolute, z-index high over the whole page) and set the keyboard focus to the document so that no other elements except the lightbox and document are likely to get input events. That is what is typically done with a lightbox overlay. If it covers the other elements (even if partially transparent), then it will intercept all mouse clicks. If you then set the keyboard focus to the document, then key events either won't ever go to the elements in the page or you can make it so it works that way. If you position the lightbox this way and prevent anything beneath the lightbox from ever getting the keyboard focus, then you should get your first propagation order.

Working example: http://jsfiddle.net/jfriend00/q3r78/

This example also has code in it that prevents the default handling of keystrokes so that you can't tab to other fields.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Interesting thing is, lightbox is absolute positioned, it's covering the whole window and its zIndex is the highest among all the elements in the structure. So is the workaround related to assigning key events onto `$(document)` instead of `$('body')` when the lightbox is there? Can you ellaborate on that, please? – inhan Sep 13 '12 at 23:50
  • @inhan - See my working example: http://jsfiddle.net/jfriend00/q3r78/. I'm not sure what question you're asking or what problem your page has if you're already putting the lightbox on top. – jfriend00 Sep 13 '12 at 23:51
  • Obviously I'm missing something. All the structure already looks the same. Let me try harder. Thanks for the demo and explanation btw. – inhan Sep 14 '12 at 00:07
  • @inhan - if the lightbox already covers everything, then mouse events will ONLY go to the lightbox. You just have to make sure that the keyboard focus gets initially set to something like the lightbox or document and that you don't let the user tab outside the lightbox/document so the keyboard focus can never get anywhere you don't want it to be. Another way to handle keyboard focus would be to set all keyboard-handling things outside the lightbox to readonly when the lightbox is up and then restore them afterwards. – jfriend00 Sep 14 '12 at 00:09
  • I finally figured out what's happening with my code and yours. Body is catching all the key events and necessary ones (that the window and lightbox uses) are also prevented: http://jsfiddle.net/q3r78/24/ You can't switch between the input fields in lightbox nor can you use application keyboard shortcuts. What do you think? – inhan Sep 14 '12 at 00:31
  • @inhan - my example was overzealous in blocking keystrokes because I didn't know what you wanted to allow. How about something more simple like this: http://jsfiddle.net/jfriend00/dtNKt/. – jfriend00 Sep 14 '12 at 00:46
0

I could not find any reliable solution so I chose to write my own function(s). Although I'm not much happy messing up with it, I had to control the behavior of the TAB key and create global keyup/keydown functions.

For those interested I found some resources in SO, here, here and here.

Note that I'm assigning a CSS class named "disabled" to the anchors in my script so you can consider those that you'll see in my code like disabled="disabled" attribute of applicable elements. Also, I could not find a way to make it focus out from the html content into the selectable UI elements of the browser but that's probably the last concern.

// lightbox keyboard navigation
function getFocusList() {
    return $('body,#lightbox *:visible').filter(function(idx,elm) {
        if ($(elm).is('body')) return true;
        if (/^hidden$/i.test($(elm).css('visibility'))) return false;
        return (
            (/^a(rea)?$/i.test(elm.nodeName) && $(elm).hasAttr('href') && !$(elm).is('.disabled')) ||
            (/^(input|textarea|button|select)$/i.test(elm.nodeName) && !$(elm).prop('disabled')) ||
            (/^\d+$/.test($(elm).prop('tabindex')) && !$(elm).prop('disabled') && !$(elm).is('.disabled'))
        )
    });
}
function globalKeyDown(e) {
    var
        LB          = !!$('#lightbox').length,
        isTAB       = e.which == 9,
        isSHIFT     = e.which == 16,
        SHIFT_ON    = LB && typeof $('#lightbox').data('SHIFT_ON') != 'undefined'
    if (isSHIFT && LB) $('#lightbox').data('SHIFT_ON',true);
    if (!isTAB || !LB) return;
    var focusList = getFocusList(), nextIndex;
    var curIndex = $.inArray($(this).get(0),focusList);
    if (SHIFT_ON) nextIndex = --curIndex == 0 ? focusList.length - 1 : curIndex;
    else nextIndex = ++curIndex == focusList.length ? (focusList.length > 1 ? 1 : 0) : curIndex;
    var next = $(focusList[nextIndex]);
    e.preventDefault();
    e.stopImmediatePropagation();
    next.focus();
}
function globalKeyUp(e) {
    var
        LB          = !!$('#lightbox').length,
        isSHIFT     = e.which == 16,
        SHIFT_ON    = LB && typeof $('#lightbox').data('SHIFT_ON') != 'undefined'
    if (isSHIFT && SHIFT_ON) $('#lightbox').removeData('SHIFT_ON')
}

$.fn.extend({
    hasAttr:function(attrStr) {
        var res = true;
        $(this).each(function() {
            if (typeof $(this).attr(attrStr) == 'undefined') res = false;
        });
        return res;
    }
});
$(document).ready(function() {
    $('body')
    .on('focus','#lightbox a',function() {$(this).addClass('hover')})
    .on('blur','#lightbox a',function() {$(this).removeClass('hover')})
    .on('keydown','*',globalKeyDown).on('keyup','*',globalKeyUp)
    .on('keydown',globalKeyDown).on('keyup',globalKeyUp)
    // remaining jQuery code...
});

I hope this saves some time for others with a similar issue.

Community
  • 1
  • 1
inhan
  • 7,394
  • 2
  • 24
  • 35