0

I'm trying to detect whether a click has occurred on a DatePicker control or not, but I can't accurately determine whether or not the event target element is a part of the DatePicker or not.

After a lot of debugging I've realized that the main issue is that pieces of the DatePicker are detached from the document, so they are simply not a child of any element.

Thanks to this other question I learned that the reason is that a chunk of the DatePicker is being removed and replaced when switching months, so the element that I get from event.target is detached (it was part of the removed chunk).

So, what can I do to determine that this click happened within my control? I'm trying to write this agnostically of what kind of content is being used, although a specific fix for DatePickers would be ok.

So far what I've done is check if my elements contain a DatePicker, and whether the event target is detached. If so, I assume that the click happened on the DatePicker. This isn't perfect because there could be another DatePicker on the page. Also, the event target being detached doesn't necessarily have to be related to DatePicker.

Here's an example of what I have so far:

$(document).on("click", function (event) {
   const stuff = $("something");

   if (stuff.is($(event.target)))          
      return; // click was on stuff

   if ($(event.target).closest(stuff).length)
      return; // click was on descendents of stuff

   if (stuff.is(":active, :focus"))
      return; // stuff is active or focused

   if (stuff.find(":active, :focus").length)
      return; // a descendent of stuff is active or focused

   if (stuff.is(".hasDatepicker") || stuff.find(".hasDatepicker").length) {
      // stuff is or has a date picker
      if (!$(event.target).closest($(document)).length) {
         // event target is detached, and there's a date picker in stuff
         // so have to assume click was on stuff's date picker
         return;
      }
   }

   // click was not within stuff's elements
}
Dave Cousineau
  • 10,300
  • 7
  • 56
  • 73
  • 1
    You need to master jQuery's [.on()](http://api.jquery.com/on/), in particular its `.on(events, selector, handler)` form. Note the presence of `selector`. Given your current level of understanding, you should get the idea almost immediately. – Roamer-1888 Jul 16 '17 at 04:03

2 Answers2

1

What you are looking for is probably $(".selector").datepicker("widget")

Per the docs:

Returns a jQuery object containing the datepicker. This method does not accept any arguments.

Code examples: Invoke the widget method:

var widget = $( ".selector" ).datepicker( "widget" );

Now that you can get the actual datepicker UI from the original element, you can use that how you like, ie to check if the click was on it.

Here is how I would solve this issue using the above:

$(function() {

  $('#thedate').datepicker({
    dateFormat: 'dd-mm-yy',
    altField: '#thealtdate',
    altFormat: 'yy-mm-dd' 
  });

  $(document).on('mousedown',function(event) {
    // only evaluate if the click was NOT on the element or any of its descendants 
    if (!$(event.target).closest('#some-container').length && !$(event.target).is('#some-container')) {
      // now lets check for any elements in our container that have datepickers 
      var $contanier = $('#some-container');
      var $elementsWithDatePickers = $contanier.find('.hasDatepicker');
      var clickedChildDatePicker = false;
      $elementsWithDatePickers.each(function() {
        // for each datepicker found, check that the click was also not on it or any of its descendants
        var $widget = $(this).datepicker("widget");
        if ($(event.target).is($widget) || $(event.target).closest($widget[0]).length > 0) {
          clickedChildDatePicker = true;
          return false;
        }
      });
      if (!clickedChildDatePicker) {
        $contanier.hide();
      }
    }
  }) 
});
#other{
  background-color:#ccc; 
  padding: 45px;
}
#some-container{
  background-color:#fff;
  padding: 45px;
  
}
<link href="https://code.jquery.com/ui/1.8.21/themes/base/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.11.3/jquery-ui.min.js"></script>


<div id="other"> 

<div id="some-container">
  Date :
  <div id="thedate">
  </div>
  <br /> 
</div>

  click here to hide container 
</div>
Wesley Smith
  • 17,890
  • 15
  • 70
  • 121
  • I have a popup that I want to hide when I "click away". I was using `focusout` before, but it didn't work with the datepicker. So I switched to `$(document).click`. Then I came up against this issue where the elements in the event have already been removed from the document so I can't actually tell which elements they were or where they came from. – Dave Cousineau Jul 16 '17 at 05:30
  • I understand this could be seen as an "XY" problem, but in asking this question I'm more curious about this specific situation of handling an event from elements that have already been removed. – Dave Cousineau Jul 16 '17 at 05:44
  • No, that's perfect, now that I understand the real issue, I can help more, see my update ;) – Wesley Smith Jul 16 '17 at 06:16
  • That should do ya, however, do note that the datepicker elements **are not** removed from the page. To prove that, do this, open the datepicker, right click a date and select "inspect element", you should see the dom element highlighted in the inspector. Now, go back and close the datepicker, note that the elements are all still shown in the inspector, they are all just hidden. Also note that they are appended to the body tag. So the elements ARE there, just hidden and not where you expect to find them in the dom. – Wesley Smith Jul 16 '17 at 06:21
  • I think the reason this works for you is that you used an `input` for the datepicker when I am using a `div`. I understand that some DOM elements are hidden which is different from being removed. But pieces of the datepicker are *removed* (not hidden) from the document when you switch months using the next and prev arrows. Try changing the `input` to a `div`, and then try clicking a next or prev arrow and you'll have the datepicker hidden because the click event fired for the missing elements. I'm not sure why it doesn't do that when it's an `input`. I may need to use a hidden `input` instead. – Dave Cousineau Jul 16 '17 at 06:28
  • @Sahuagin ah, yeah indeed, inline datepickers are handled a little differently. I agree, if you can use input/popover style, that would probably be best – Wesley Smith Jul 16 '17 at 07:41
  • @Sahuagin actually, after some tweaking, changing the handler to `$(document).on('mousedown',function(event) {` will allow it to work with both the popup and inline style datepickers, see the updated example above – Wesley Smith Jul 16 '17 at 08:30
  • Maybe I'm missing something, but is jQuery UI datepicker's natural behaviour not to hide when user clicks-away? That's certainly the behaviour I see [here](https://jsfiddle.net/cc2sg2er/) in desktop Chrome & IE. – Roamer-1888 Jul 16 '17 at 12:14
  • @Roamer-1888 yeah but the op has the date picker inside s popup window, the OP wants the popup window itself to close when you click outside the the popup window ( like bootstrap modals do) unfortunately due to the way jquery UI adds elements, the inline date picker breaks that for the OP, this fixes the click detection issue – Wesley Smith Jul 16 '17 at 14:01
  • @DelightedD0D, I can find where the OP says he wants to use a div instead of an input but nothing about that datepicker appearing in a popup. You may be right yet, but the way I read it he just wants a datepicker based on a read-only element. If so, then a disabled input is ideal (the widget is designed to be used with ``s) but needs [this workaround](https://stackoverflow.com/a/3100395/3478010) to make it clickable (disabled elements don't fire mouse events) ... and the javascript becomes painfully simple. Anyways, even if I'm wrong, [here's a demo](https://jsfiddle.net/jfosfLv2/1/). – Roamer-1888 Jul 16 '17 at 17:22
  • @Roamer-1888 check out the OPs first comment above: *"I have a popup that I want to hide when I "click away". I was using focusout before, but it didn't work with the datepicker. So I switched to $(document).click..."* That's what made me understand. As an example see http://jsfiddle.net/DelightedDoD/8w8v9/3290/. Consider the white box the "popup". note that if you click anywhere outside the white box, the white will be hidden right? – Wesley Smith Jul 16 '17 at 21:34
  • @Roamer-1888 Now, put a date picker inside that "popup" and click one of the dates in the picker, http://jsfiddle.net/DelightedDoD/8w8v9/3291/ note that the popup is hidden? That shouldnt happen, clicking a date inside the datepicker **should** count as a click **inside** the popup and *not* hide it but the inline datepicker breaks that. To fix that, we can use *mousedown* instead of *click* like this: http://jsfiddle.net/DelightedDoD/8w8v9/3292/ – Wesley Smith Jul 16 '17 at 21:37
  • @Roamer-1888 also, your assertion about datepickers, *"the widget is designed to be used with s"*, is not quite accurate, [per the docs:](https://api.jqueryui.com/1.12/datepicker/)"By default, the datepicker calendar opens in a small overlay when the associated text field gains focus. For an inline calendar, simply attach the datepicker to a div or span." The OP is using an inline datepicker, one that is always open on the page, and they do act a bit differently with regards to how they are added to the DOM – Wesley Smith Jul 16 '17 at 21:45
  • @DelightedD0D, your exlanation is 100% coherent and probably correct. Let's wait for the OP to come back. – Roamer-1888 Jul 16 '17 at 22:36
  • @DelightedD0D @Roamer-1888 sorry for the delay, was on days off. switching events has definitely fixed it, many thanks. I ultimately went with the `mouseup` event which worked a bit better with some things. (it's up to you, but is it possible you could reword your answer slightly? it doesn't really have anything to do with using the widget method, the two solutions are: use an `input` element instead of a `div`, or use `mouseup` or `mousedown` events instead of `click`.) – Dave Cousineau Jul 20 '17 at 23:31
  • @Sahuagin, your mysterious popup - is it a popup element that contains a datepicker and other elements, or is it the datepicker itself? – Roamer-1888 Jul 20 '17 at 23:53
  • @Roamer-1888 The case where I was having this issue was just the DatePicker element itself (a `div` which had `datepicker()` called on it). But, my popup code is separate from my DatePicker wrapper code, and I'm also trying to keep the "click away" code separate from both of those. – Dave Cousineau Jul 21 '17 at 00:04
  • If DelightedD0D's solution works then the best thing to do is run with it though I still have a feeling that things can be simplified, maybe significantly, but without full sight of all the code, it's impossible to advsie. – Roamer-1888 Jul 21 '17 at 00:11
  • 1
    @Roamer-1888 Well, there's too much. My code is actually all in TypeScript for one thing, with my own constantly expanding control framework. What I posted was an attempt an a "minimal, complete, verifiable example". It doesn't really have anything to do with popups, the question really is "how do I get around a `div` DatePicker behaving strangely with `click` events". One answer is, "use an `input` or use a different event". – Dave Cousineau Jul 21 '17 at 00:20
-1

I don't get you point about:
« pieces of the DatePicker are detached from the document, so they are simply not a child of any element.»

That is wrong. All element you can see in a page are in DOM, (so "attached" to the document).
But sometimes, not for a very long time, which is the case here.

The Datepicker elements are created on open and attached to body.
When it closes, its elements are all removed.

The code below retreives the clicked element, it tagName, its class, its id and its innerHTML.
I even re-created them in the document...
;)

It uses delegation, as @Roamer correctly mentionned as needed in comments.

$(document).on("mouseup",".ui-datepicker",function(e){
  var target = e.target;

  var tag = target.tagName.toLowerCase();
  var classes = target.className;
  var ID = target.id;
  var resultString = "<"+tag+" id='"+ID+"' class='"+classes+"'>"+target.innerHTML+"</"+tag+"><br>";
  console.log(resultString);

  var result = $(resultString);
  $("#result").append(result);
});

Sadly, the CSS of my example does not work well in a SO snippet. So it is on CodePen.

I used the mouseup event, since click doesn't work...
Probably due to a return false somewhere in Datepicker.

Louys Patrice Bessette
  • 27,837
  • 5
  • 32
  • 57
  • *pieces of the DatePicker are detached from the document*, what the OP means is that the actual datepicker is appended to the document body and not the parent of the element it is bound to therefore the original element and the actual datepicker ui don't share a parent – Wesley Smith Jul 16 '17 at 04:29
  • 1
    @DelightedD0D: Your comprehension of the issue is correct. But the second part of the quoted sentence still is wrong : *«they are simply not a child of any element.»*. – Louys Patrice Bessette Jul 16 '17 at 04:34
  • Oh yeah, absolutely agreed there. The OP doesnt quite understand what happens there, just that it's not what they expect – Wesley Smith Jul 16 '17 at 04:36
  • 1
    The only thing that makes any sense would be "... the main issue is that pieces of the DatePicker are removed *and re-created at some point in the future*", hence the loss of attached event handlers and the need to delegate. At least, that's the way I see it. – Roamer-1888 Jul 16 '17 at 05:08
  • 1
    @Roamer: I added some explanation inspired by your comment ;) – Louys Patrice Bessette Jul 16 '17 at 05:18
  • What I mean by "they are not a child" is that if you go up the tree you hit `null` before you get to `body` or `html`. `event.target.parentElement.parentElement.parentElement` is null; it's just an orphaned `div`. – Dave Cousineau Jul 16 '17 at 05:26
  • It is a child of `body`, when it is opened. But, yeah, you won't find it starting fron the `input` and looking all parents. Please open the datepicker and hit CTRL-SHIFt-C and click on it to see where it actually is. --- Enought discussion on this. – Louys Patrice Bessette Jul 16 '17 at 05:31
  • In what way is it a child of `body`? The problem is that these elements have been removed from the document tree and are orphaned. If you go up the tree you get something like `span -> a -> div -> div -> null`. – Dave Cousineau Jul 16 '17 at 05:35
  • Okay Sahuagin... Maybe a [**printscreen**](http://imgur.com/a/8pPck) will convince you. I don't know how to explain it in another way. It is obvious to me. – Louys Patrice Bessette Jul 16 '17 at 05:39
  • Ok, now handle `click`, click on one of the next or prev arrow buttons, break the debugger in the event handler, and inspect `event.target`. Specifically, look at `event.target.parentElement` and it's parent, and it's parent and so on. It hits `null` after 4 or 5 elements, way before `body`. What you are showing me are the elements on the page, not the elements in `event.target`. – Dave Cousineau Jul 16 '17 at 05:42
  • Ok, I get your idea... But the `NULL` you are getting is certainly due to a limitation on jQuery to grab ALL parents of a target, I don't know. The elements you are exploring, in fact, is an object contained in a variable, produced from the DOM, not the real DOM. It may not be complete. But what is shown in the prinstcreen IS the real DOM and is complete. – Louys Patrice Bessette Jul 16 '17 at 05:48
  • [Look here](https://api.jquery.com/category/events/event-object/). *«Most properties from the original event are copied over and normalized to the new event object.»* – Louys Patrice Bessette Jul 16 '17 at 05:55