Forums

The forums ran from 2008-2020 and are now closed and viewable here as an archive.

Home Forums JavaScript Touch device ghost click triggers on different element

  • This topic is empty.
Viewing 15 posts - 1 through 15 (of 26 total)
  • Author
    Posts
  • #249338
    Shikkediel
    Participant

    Anyone happened to come across the following on other OSes than Android 2.3?

    Instead of listening to clicks on touch devices, to avoid the 300ms delay one can program their own sequence of a touchstart > touchend and use that as if they were clicks. So far so good.

    But in my test case, I’m also listening to mousedown and mouseup to achieve something similar on desktop.

    The touch events will also automatically emulate these mouse events (including a click) though so some filtering has to take place there to avoid firing any functions twice.

    But here it comes… say I have a button that I click to show a modal. This modal will end up on top of the button after applying a jQuery show(). Now what happens on Android 2.3 is that the emulated “ghost” events aren’t fired on the original element that was clicked but on the modal!

    Same thing takes place when hiding that model, clicks to close it will trigger events on any element that is underneath the spot that was originally clicked…

    I have managed a workaround but is this familiar to anyone? If so, I’d have to expand the browsers I’m targeting with the code.

    Thanks in advance.

    $(document).on('mousedown touchstart', subject, function(e) {
    
      if (e.type == 'mousedown' && e.which != 1) return;
      var mean = $(e.currentTarget);
    
      mean.one('mouseup touchend', function(e) {
        if (e.type == 'touchend') $('body').addClass('punch');
        else if ($('body').hasClass('punch')) return;
        mean.trigger('page:tap');
      });
    
    })
    .on('click', subject, function() {
    
      $('body').removeClass('punch');
      return false;
    });
    

    Including the code for completeness. I’m triggering a custom page:tapevent there on any elements I’ve added to the variablesubject but that’s not all that relevant. The workaround there is to add a class to body while a touch event takes place and remove that class when the click is emulated. Rather circumventive.

    #249339
    Shikkediel
    Participant

    Never mind that I could leave out the touch events altogether of course. I’d rather handle them… good practice too. And it’s quicker. Although that OS is giving me minor fits on occasion. I almost think they released it before they were really finished.

    :-S

    But if code works on that, it’ll work on pretty much anything. Probably my toaster too.

    #249349
    Shikkediel
    Participant

    Let me summarise what happens, touch events can be a bit mind boggling. And their behaviour isn’t completely predicatable either.

    • touchstart and touchend take place
    • page:tap gets triggered
    • modal opens
    • mousedown, mouseup and clickare emulated at same position
    • modal is now there, events get triggered on it

    I’ll be using the aforementioned code, it’ll work for all touch devices (with some minimal adjustment for devices that can also handle a mouse). This will prevent another page:tap happening on the modal because of the emulated events. But it’s still strange the events aren’t triggered on the original element.

    #249453
    Dendiwiguna
    Participant

    Thank you for the information

    #252780
    Shikkediel
    Participant

    Apparently needed a small adaptation…

    $(document).on('mousedown touchstart', subject, function(e) {
    
      if (e.type == 'mousedown' && e.which != 1) return;
      var mean = $(e.currentTarget);
    
      mean.one('mouseup touchend', function(e) {
        if (e.type == 'touchend' && !$('body').hasClass('punch')) $('body').addClass('punch');
        else if ($('body').hasClass('punch')) return;
        mean.trigger('page:tap');
      });
    
    })
    .on('click', subject, function() {
    
    $('body').removeClass('punch');
      return false;
    });
    

    Couldn’t find out why multiple touchend events were firing but this prevents triggering all but one.

    #252781
    Shikkediel
    Participant

    Could a mod please add the rejected post? @Paulie_D @Senff

    I think I may have edited too many times or something, altering code characters and indentations…

    #252826
    Shikkediel
    Participant

    Never mind then, I’ll try posting the update again:

    $(document).on('mousedown touchstart', subject, function(e) {
    
      if (e.type == 'mousedown' && e.which != 1) return;
      var mean = $(e.currentTarget);
    
      mean.one('mouseup touchend', function(e) {
        if (e.type == 'touchend' && !$('body').hasClass('punch')) $('body').addClass('punch');
        else if ($('body').hasClass('punch')) return;
        mean.trigger('page:tap');
      });
    
    })
    .on('click', subject, function() {
    
      $('body').removeClass('punch');
      return false;
    });
    

    I think the ghost clicks might have been inexplicable extra touchend events.
    This will make sure only a single one is actively triggering.

    #252882
    Shikkediel
    Participant

    I think the ghost clicks might have been inexplicable extra touchend events.

    Nope, the original issue still persists but the custom event itself’s working a lot smoother now.

    #252907
    Mottie
    Member

    I’ve heard that this solution from Stackoverflow takes care of the ghost click:

    $(document).on('touchstart click', '.myBtn', function(event){
        if(event.handled === false) return
        event.stopPropagation();
        event.preventDefault();
        event.handled = true;
        // Do your magic here
    });
    

    Additionally, you might want to consider using pointer events… jQuery’s polyfill PEP is useful in this case. Check out the Why pointer events section in the readme!

    #252913
    Shikkediel
    Participant

    Thanks, some interesting reading material there. Much appreciated.

    I can’t see how that code could work without event.handled ever being set to false though. Unless I’m unaware of a native .handled that automatcially resets the variable after click somehow…

    This whole thing is a lot more complicated than it might look at first, I found out.
    Pointer events are definitely something I’ll be looking into.

    #252914
    Shikkediel
    Participant

    I think I like your approach better by the way, it is similar to what I’ve fiddled together. But instead of using a timeout, I might check the event type and flip the variable on the basis of that.

    My old Android phone is utterly slow and I noticed it can take up to 500ms to handle eveything from the touchend to the final click. Especially when Google play starts looking for updates simultaneously.

    #252915
    Shikkediel
    Participant

    Okay, I think I finally got to the bottom of this. There are two issues crossing each other. The first one is the more generally known ghost clicks, these seem to be handled nicely from where I got so far. The other issue is very Android specific and hard to describe. After a custom (click) event that deals with both touch and mouse and then hiding the element in question, the emulated mouse events caused by touch devices are triggered on the element that was underneath it. And not on the element itself. This is clearly a bug that needs an approach of it’s own – I chose to use elementFromPoint to determine which element is underneath and temporarily disable pointer events on it. Strangely enough links still get highlighted because of an emulated hover effect now and then but I can live with that.

    So I’m sending some extra data with the custom page:tap event (did I ever mention how excellent this part of jQuery is?), namely the touch coordinates:

    var nub = $('html'), swiftClick = function(aim) {
    
      $(bud).on('mousedown touchstart', aim, function(e) {
    
        if (e.type == 'mousedown' && e.which != 1) return;
    
        var mean = $(e.currentTarget);
    
        if (e.type == 'touchstart') {
          var feel = e.originalEvent.touches[0],
          region = [feel.screenX, feel.screenY];
        }
    
        mean.one('mouseup touchend', function(e) {
    
          var hit = nub.hasClass('punch');
    
          if (e.type == 'touchend' && !hit) nub.addClass('punch');
          else if (hit) return;
    
          mean.trigger({type: 'page:tap', put: region});
        });
    }).on('click', aim, function() {
    
      if (!nub.hasClass('punch')) return;
      nub.removeClass('punch');
      return false;
    });
    }
    

    Also troublesome on Android, nothing but screenX and screenY did the trick.

    Next step is to listen to the custom event, check the coordinates and “silence” the element below if it is a link (closing the modal would trigger a redirect before):

    var goal = '#anelement';
    
    swiftClick(goal);
    
    $(goal).on('page:tap', function(e) {
    
      if (!e.put) return;
    
      var below = document.elementFromPoint(e.put[0], e.put[1]),
      sample = $(below).closest('a');
    
      if (!sample) return;
    
      sample.addClass('silent');
    
      setTimeout(function() {
        sample.removeClass('silent');
      }, 500);
    });
    
    .silent {
      pointer-events: none;
    }
    

    Very circumventive but that’s old Android for ya…

    #252916
    Shikkediel
    Participant

    Small addendum. By adding the punch class to html or body, the Android issue is pretty much resolved because all elements listen to the same page:tap (event on the element below will then not get triggered). Adding the class to the element itself would not work in that case. The extra bit of code that silences the element below is mostly effective now for not getting the link underneath hightlighted because of an emulated hover effect. Which I think occurs with mouseup, meaning that specific emulated event sometimes takes place more than 500ms after the initial touchstart… slightly annoying but otherwise to no real effect.

    #252918
    Mottie
    Member

    I think the event.handled works in the script I posted because the event is provided by a delegated binding, so the event is propagating up the DOM tree, so the event.handled flag remains.

    Another idea, used by hammer-time (also the makers of hammer.js), is to add a touch-action:none; css property to the element – which does appear to be supported by all modern browsers now. Their script adds the style in-line, but I don’t see why you couldn’t toggle a class name to apply it.

    #252920
    Shikkediel
    Participant

    I think the event.handled works in the script I posted because the event is provided by a delegated binding, so the event is propagating up the DOM tree, so the event.handled flag remains.

    That is an interesting suggestion but I do not see how the code could ever set the variable to false. It starts as undefined and after the first event becomes true indefinitely. So the initial condition can never be fulfilled. Unless I’m missing something…

    Edit – seems to always be undefined:

    codepen.io/anon/pen/KWZeeg

Viewing 15 posts - 1 through 15 (of 26 total)
  • The forum ‘JavaScript’ is closed to new topics and replies.