6

I'm trying to stop a particular click event from bubbling to document-root, which in result closes one of my popup. I need to stop bubbling of the event and body or html are my only options to intercept and stop it.

The date-picker popup is generated on the fly so I cannot use a direct event on .ui-icon element, so I have registered a delegate event on body element to stop it from bubbling.

(function ($) {
    $(function () {
        $('body').on('click', '.ui-icon', function (e) {
            e.stopPropagation();
        });
    });
})(jQuery);

Surprisingly enough registering a direct event to body element and checking the event's target works just fine.

(function ($) {
    $(function () {
        $('body').on('click', function (e) {
            if ($(e.target).is('.ui-icon')) {
                e.stopPropagation();
            }
        });
    });
})(jQuery);

I am really at a loss, why the previous one does not work where the later does, both of them are supposed to do the same. What am I missing? It might have to do with jQuery datepicker getting recomposed (its whole content block is rebuilt on navigation) before the event reaches body (but it does not make sense)?

Snippet with the issue is added below. I just want the arrows (datepicker navigation) to stop bubbling to document/root level (which closes my popup) and because datepicker gets appended to body, the only available intercept points are body/html.

$(function() {
  let popup = $('#some-popup').addClass('visible');
  let input = $('#some-date');
  let toggler = $('#toggler');

  // binding popup
  toggler.on('click', function(e) {
    e.stopPropagation();
    popup.toggleClass('visible');
  });

  // initializing jQuery UI datepicker
  input.datepicker();

  // closing popup on document clicks other than popup itself
  $(document).on('click', function(e) {
    let target = $(e.target);

    if (target.is('.ui-icon, .ui-datepicker-prev, .ui-datepicker-next')) {
      console.warn('shouldn\'t have reached this, got: ' + target.attr('class'));
    }

    if (!(target.is('#some-popup'))) {
      popup.removeClass('visible');
    }
  });

  // trying to prevent click from reaching document
  $('body').on('click', '.ui-icon, .ui-datepicker-prev, .ui-datepicker-next', function(e) {
    e.stopPropagation();
  })
});
#some-popup {
  padding: 15px 25px;
  background: #000;
  color: #fff;
  display: none;
  max-width: 200px;
}

#some-popup.visible {
  display: block;
}

#toggler {
  margin-bottom: 10px;
}
<head>
  <link href="https://code.jquery.com/ui/1.11.4/themes/black-tie/jquery-ui.css" rel="stylesheet">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
</head>

<body>

  <div id="some-popup">
    This is the popup
  </div>
  <button type="button" id="toggler">Show/Hide Popup</button>
  <form>
    <label for="some-date">The Date-Picker</label>
    <input id="some-date" onclick="event.stopPropagation();" />
  </form>
</body>
Ravenous
  • 1,112
  • 11
  • 25
  • Do you have both of those $('body').on('click') in your code? And you are saying the second one does not fire? Have you tried removing the first body onclick and seeing if the second one fires? – Ryan Wilson Mar 13 '18 at 16:10
  • That's correct. – Ravenous Mar 13 '18 at 16:11
  • Remove the first body onclick and try it with just the second body onclick that you have commented as does not work – Ryan Wilson Mar 13 '18 at 16:12
  • Can you confirm that the '.icon' exists in html? It's not trying to bind to something that doesn't exist yet? – Amos47 Mar 13 '18 at 16:12
  • 1
    https://jsfiddle.net/radyqbLg/2/ I mean it seems to work – Taplar Mar 13 '18 at 16:13
  • Got home and rechecked my post and I was wrong, added a snippet of my particular scenario. – Ravenous Mar 13 '18 at 21:17
  • Why mess with event propagation at all? You know what elements you don't want to listen to, so instead of `console.log` in that `if`, just `return;` – Heretic Monkey Mar 13 '18 at 21:23
  • Also, the order of event binding does matter: see [stopPropagation vs. stopImmediatePropagation](//stackoverflow.com/a/5299841) – Heretic Monkey Mar 13 '18 at 21:26
  • @MikeMcCaughan, well, that's just what I did in the first snippet and it worked, but I'm curious why the other one did not. I know how stopPropagation or stopImmediatePropagation work and that order of event binding matters (first come, first served, as mentioned in the initial post), but even isolated (one binding at a time), the first version works, the second does not. – Ravenous Mar 13 '18 at 21:34
  • If it's a matter of bubbling phase not propagating to the body but you want to keep `stopPropagation()` for some reason, try using `eventListener` and set it to listen on capture phase instead of bubbling phase by setting the 3rd parameter to true. – zer00ne Mar 18 '18 at 18:50

1 Answers1

4

The delegate event does not fire because when you click on the arrows, at first the arrow button's click event is fired where jquery-ui-datepicker removes the whole calendar element from body element and generates a new calendar for previous/next month.

You can verify if an element is removed or not by checking if the element has any parent <body> tag i.e by checking the length of closest('body').

    $('body').on('click', function (e) {
        if ($(e.target).is('.ui-icon')) {
            console.log($(e.target).closest('body').length);
            // Prints 0 i.e this element is not a child of <body>
        }
    });

To fire the delegate event, the target element must be a child element of the event bound element, otherwise jQuery does not trigger the event. The following Demo confirms it.

$(function() {
  $('.a').on('click', function() {
    console.log('Direct Event');
  })
  $('.a').on('click', '.b,.c', function(e) {
    console.log('Delegate Event');
  })
  $('.b').on('click', function() {
    console.log('datepicker arrow event');
    if($('input').is(':checked')) $(this).remove();
  })
});
.a {
  padding: 20px;
  background: #ffc55a;
  text-align: center;
}
.b {
  margin-top: 5px;
  padding: 5px;
  background: #7ddbff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="a">
  This is the parent element (Delegate Bound Element)
  <div class="b">
    This is the Arrow element of date-picker (Delegate Target)<br>
    Click on it to see how many events fire
  </div>
</div>
<input type="checkbox" checked> Remove On<br>
With Remove Off 3 events fire<br>
With Remove On 2 events fire, Delegate event does not fire<br>
Munim Munna
  • 17,178
  • 6
  • 29
  • 58
  • So, it is how I said, the fact that those elements get recomposed on navigation, so you are saying that if I click X and X gets removed from DOM before reaching the delegated parent, the event won't fire. Is this correct? I don't get what you are on about being a child of body, it is a child of body, not a direct child, but a child in terms of event bubbling. – Ravenous Mar 16 '18 at 11:41
  • 1
    It is a child of body when you click on the icon, then datepicker next event fires, that event removes the element from document, so when the event reaches body, the icon is no longer child of body, so delegate does not fire. – Munim Munna Mar 16 '18 at 11:49
  • 1
    @MunimMunna : As you mentioned when next event fires element is removed from the document. but if you write the same code on document click instead of body click it gets called. – Makarand Patil Mar 16 '18 at 12:03
  • I think the click event does bubble up to document/root level, but event delegation "validation" when reaching body is off cause the DOM element is no longer a child of the delegated parent. – Ravenous Mar 16 '18 at 12:10
  • @MakarandPatil is right, I should have said, it is removed from body, not from document. Any element removed from body still remains child to the document, and is capable of firing delegate event on document. – Munim Munna Mar 16 '18 at 16:43